6152 lines
229 KiB
Python

import os
import sys
import logging
import atexit
from datetime import datetime, timedelta
from flask import Flask, render_template, request, jsonify, redirect, url_for, flash, send_file, abort, session, make_response
from flask_login import LoginManager, login_user, logout_user, login_required, current_user
from flask_wtf import CSRFProtect
from flask_wtf.csrf import CSRFError
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 concurrent.futures import ThreadPoolExecutor, as_completed
from typing import List, Dict, Tuple
import time
import subprocess
import json
import signal
# Windows-spezifische Fixes früh importieren (sichere Version)
if os.name == 'nt':
try:
from utils.windows_fixes import get_windows_thread_manager
# apply_all_windows_fixes() wird automatisch beim Import ausgeführt
print("✅ Windows-Fixes (sichere Version) geladen")
except ImportError as e:
# Fallback falls windows_fixes nicht verfügbar
get_windows_thread_manager = None
print(f"⚠️ Windows-Fixes nicht verfügbar: {str(e)}")
else:
get_windows_thread_manager = None
# Lokale Imports
from models import init_database, create_initial_admin, User, Printer, Job, Stats, SystemLog, get_db_session, GuestRequest, UserPermission, Notification
from utils.logging_config import setup_logging, get_logger, measure_execution_time, log_startup_info, debug_request, debug_response
from utils.job_scheduler import JobScheduler, get_job_scheduler
from utils.queue_manager import start_queue_manager, stop_queue_manager, get_queue_manager
from config.settings import SECRET_KEY, UPLOAD_FOLDER, ALLOWED_EXTENSIONS, ENVIRONMENT, SESSION_LIFETIME, SCHEDULER_ENABLED, SCHEDULER_INTERVAL
# Blueprints importieren
from blueprints.guest import guest_blueprint
from blueprints.calendar import calendar_blueprint
from blueprints.users import users_blueprint
# Scheduler importieren falls verfügbar
try:
from utils.job_scheduler import scheduler
except ImportError:
scheduler = None
# SSL-Kontext importieren falls verfügbar
try:
from utils.ssl_config import get_ssl_context
except ImportError:
def get_ssl_context():
return None
# Template-Helfer importieren falls verfügbar
try:
from utils.template_helpers import register_template_helpers
except ImportError:
def register_template_helpers(app):
pass
# Datenbank-Monitor und Backup-Manager importieren falls verfügbar
try:
from utils.database_monitor import DatabaseMonitor
database_monitor = DatabaseMonitor()
except ImportError:
database_monitor = None
try:
from utils.backup_manager import BackupManager
backup_manager = BackupManager()
except ImportError:
backup_manager = None
# Import neuer Systeme
from utils.rate_limiter import limit_requests, rate_limiter, cleanup_rate_limiter
from utils.security import init_security, require_secure_headers, security_check
from utils.permissions import init_permission_helpers, require_permission, Permission, check_permission
from utils.analytics import analytics_engine, track_event, get_dashboard_stats
# Flask-App initialisieren
app = Flask(__name__)
app.secret_key = SECRET_KEY
app.config["PERMANENT_SESSION_LIFETIME"] = SESSION_LIFETIME
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["WTF_CSRF_ENABLED"] = True
# CSRF-Schutz initialisieren
csrf = CSRFProtect(app)
# Security-System initialisieren
app = init_security(app)
# Permission Template Helpers registrieren
init_permission_helpers(app)
# Template-Helper registrieren
register_template_helpers(app)
# CSRF-Error-Handler - Korrigierte Version für Flask-WTF 1.2.1+
@app.errorhandler(CSRFError)
def csrf_error(error):
"""Behandelt CSRF-Fehler und gibt detaillierte Informationen zurück."""
app_logger.error(f"CSRF-Fehler für {request.path}: {error}")
if request.path.startswith('/api/'):
# Für API-Anfragen: JSON-Response
return jsonify({
"error": "CSRF-Token fehlt oder ungültig",
"reason": str(error),
"help": "Fügen Sie ein gültiges CSRF-Token zu Ihrer Anfrage hinzu"
}), 400
else:
# Für normale Anfragen: Weiterleitung zur Fehlerseite
flash("Sicherheitsfehler: Anfrage wurde abgelehnt. Bitte versuchen Sie es erneut.", "error")
return redirect(request.url)
# Blueprints registrieren
app.register_blueprint(guest_blueprint)
app.register_blueprint(calendar_blueprint)
app.register_blueprint(users_blueprint)
# Login-Manager initialisieren
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = "login"
login_manager.login_message = "Bitte melden Sie sich an, um auf diese Seite zuzugreifen."
login_manager.login_message_category = "info"
@login_manager.user_loader
def load_user(user_id):
"""
Robuster User-Loader mit Error-Handling für Schema-Probleme.
"""
try:
# user_id von Flask-Login ist immer ein String - zu Integer konvertieren
try:
user_id_int = int(user_id)
except (ValueError, TypeError):
app_logger.error(f"Ungültige User-ID: {user_id}")
return None
db_session = get_db_session()
# Robuste Abfrage mit Error-Handling
try:
user = db_session.query(User).filter(User.id == user_id_int).first()
db_session.close()
return user
except Exception as db_error:
# Schema-Problem - versuche manuelle Abfrage
app_logger.warning(f"Schema-Problem beim User-Load für ID {user_id_int}: {str(db_error)}")
# Manuelle Abfrage nur mit Basis-Feldern
try:
result = db_session.execute(
text("SELECT id, email, password_hash, name, role, active FROM users WHERE id = :user_id"),
{"user_id": user_id_int}
).fetchone()
if result:
# Manuell User-Objekt erstellen
user = User()
user.id = result[0]
user.email = result[1] if len(result) > 1 else f"user_{user_id_int}@system.local"
user.password_hash = result[2] if len(result) > 2 else ""
user.name = result[3] if len(result) > 3 else f"User {user_id_int}"
user.role = result[4] if len(result) > 4 else "user"
user.active = result[5] if len(result) > 5 else True
# Standard-Werte für fehlende Felder
user.username = getattr(user, 'username', user.email.split('@')[0])
user.created_at = getattr(user, 'created_at', datetime.now())
user.last_login = getattr(user, 'last_login', None)
user.updated_at = getattr(user, 'updated_at', datetime.now())
db_session.close()
return user
except Exception as manual_error:
app_logger.error(f"Auch manuelle User-Abfrage fehlgeschlagen: {str(manual_error)}")
db_session.close()
return None
except Exception as e:
app_logger.error(f"Kritischer Fehler im User-Loader für ID {user_id}: {str(e)}")
return None
# Jinja2 Context Processors
@app.context_processor
def inject_now():
"""Inject the current datetime into templates."""
return {'now': datetime.now()}
# Custom Jinja2 filter für Datumsformatierung
@app.template_filter('format_datetime')
def format_datetime_filter(value, format='%d.%m.%Y %H:%M'):
"""Format a datetime object to a German-style date and time string"""
if value is None:
return ""
if isinstance(value, str):
try:
value = datetime.fromisoformat(value)
except ValueError:
return value
return value.strftime(format)
# Logging initialisieren
setup_logging()
log_startup_info()
# Logger für verschiedene Komponenten
app_logger = get_logger("app")
auth_logger = get_logger("auth")
jobs_logger = get_logger("jobs")
printers_logger = get_logger("printers")
user_logger = get_logger("user")
kiosk_logger = get_logger("kiosk")
# HTTP-Request/Response-Middleware für automatisches Debug-Logging
@app.before_request
def log_request_info():
"""Loggt detaillierte Informationen über eingehende HTTP-Anfragen."""
# Nur für API-Endpunkte und wenn Debug-Level aktiviert ist
if request.path.startswith('/api/') or app_logger.level <= logging.DEBUG:
debug_request(app_logger, request)
@app.after_request
def log_response_info(response):
"""Loggt detaillierte Informationen über ausgehende HTTP-Antworten."""
# Nur für API-Endpunkte und wenn Debug-Level aktiviert ist
if request.path.startswith('/api/') or app_logger.level <= logging.DEBUG:
# Berechne Response-Zeit aus dem g-Objekt wenn verfügbar
duration_ms = None
if hasattr(request, '_start_time'):
duration_ms = (time.time() - request._start_time) * 1000
debug_response(app_logger, response, duration_ms)
return response
# Start-Zeit für Request-Timing setzen
@app.before_request
def start_timer():
"""Setzt einen Timer für die Request-Bearbeitung."""
request._start_time = time.time()
# Sicheres Passwort-Hash für Kiosk-Deaktivierung
KIOSK_PASSWORD_HASH = generate_password_hash("744563017196A")
print("Alle Blueprints wurden in app.py integriert")
# Custom decorator für Job-Besitzer-Check
def job_owner_required(f):
@wraps(f)
def decorated_function(job_id, *args, **kwargs):
db_session = get_db_session()
job = db_session.query(Job).filter(Job.id == job_id).first()
if not job:
db_session.close()
return jsonify({"error": "Job nicht gefunden"}), 404
is_owner = job.user_id == int(current_user.id) or job.owner_id == int(current_user.id)
is_admin = current_user.is_admin
if not (is_owner or is_admin):
db_session.close()
return jsonify({"error": "Keine Berechtigung"}), 403
db_session.close()
return f(job_id, *args, **kwargs)
return decorated_function
# Custom decorator für Admin-Check
def admin_required(f):
@wraps(f)
@login_required
def decorated_function(*args, **kwargs):
app_logger.info(f"Admin-Check für Funktion {f.__name__}: User authenticated: {current_user.is_authenticated}, User ID: {current_user.id if current_user.is_authenticated else 'None'}, Is Admin: {current_user.is_admin if current_user.is_authenticated else 'None'}")
if not current_user.is_admin:
app_logger.warning(f"Admin-Zugriff verweigert für User {current_user.id if current_user.is_authenticated else 'Anonymous'} auf Funktion {f.__name__}")
return jsonify({"error": "Nur Administratoren haben Zugriff"}), 403
return f(*args, **kwargs)
return decorated_function
# ===== AUTHENTIFIZIERUNGS-ROUTEN (ehemals auth.py) =====
@app.route("/auth/login", methods=["GET", "POST"])
def login():
if current_user.is_authenticated:
return redirect(url_for("index"))
error = None
if request.method == "POST":
# Unterscheiden zwischen JSON-Anfragen und normalen Formular-Anfragen
is_json_request = request.is_json or request.headers.get('Content-Type') == 'application/json'
# Daten je nach Anfrageart auslesen
if is_json_request:
data = request.get_json()
username = data.get("username") or data.get("email") # Fallback für email
password = data.get("password")
remember_me = data.get("remember_me", False)
else:
# Korrigierte Feldnamen - Template verwendet "email" nicht "username"
username = request.form.get("email") # Geändert von "username" zu "email"
password = request.form.get("password")
remember_me = request.form.get("remember_me") == "on" # Geändert von "remember-me"
if not username or not password:
error = "Benutzername und Passwort müssen angegeben werden."
if is_json_request:
return jsonify({"error": error}), 400
else:
try:
db_session = get_db_session()
# Suche nach Benutzer mit übereinstimmendem Benutzernamen oder E-Mail
user = db_session.query(User).filter(
(User.username == username) | (User.email == username)
).first()
if user and user.check_password(password):
# Update last login timestamp
user.update_last_login()
db_session.commit()
login_user(user, remember=remember_me)
auth_logger.info(f"Benutzer {username} hat sich angemeldet")
next_page = request.args.get("next")
db_session.close()
if is_json_request:
return jsonify({"success": True, "redirect_url": next_page or url_for("index")})
else:
if next_page:
return redirect(next_page)
return redirect(url_for("index"))
else:
error = "Ungültiger Benutzername oder Passwort."
auth_logger.warning(f"Fehlgeschlagener Login-Versuch für Benutzer {username}")
db_session.close()
if is_json_request:
return jsonify({"error": error}), 401
except Exception as e:
# Fehlerbehandlung für Datenbankprobleme
error = "Anmeldefehler. Bitte versuchen Sie es später erneut."
auth_logger.error(f"Fehler bei der Anmeldung: {str(e)}")
if is_json_request:
return jsonify({"error": error}), 500
return render_template("login.html", error=error)
@app.route("/auth/logout", methods=["GET", "POST"])
@login_required
def auth_logout():
"""Meldet den Benutzer ab."""
app_logger.info(f"Benutzer {current_user.email} hat sich abgemeldet")
logout_user()
flash("Sie wurden erfolgreich abgemeldet.", "info")
return redirect(url_for("login"))
@app.route("/auth/reset-password-request", methods=["GET", "POST"])
def reset_password_request():
"""Passwort-Reset anfordern (Placeholder)."""
# TODO: Implement password reset functionality
flash("Passwort-Reset-Funktionalität ist noch nicht implementiert.", "info")
return redirect(url_for("login"))
@app.route("/auth/api/login", methods=["POST"])
def api_login():
"""API-Login-Endpunkt für Frontend"""
try:
data = request.get_json()
if not data:
return jsonify({"error": "Keine Daten erhalten"}), 400
username = data.get("username")
password = data.get("password")
remember_me = data.get("remember_me", False)
if not username or not password:
return jsonify({"error": "Benutzername und Passwort müssen angegeben werden"}), 400
db_session = get_db_session()
user = db_session.query(User).filter(
(User.username == username) | (User.email == username)
).first()
if user and user.check_password(password):
# Update last login timestamp
user.update_last_login()
db_session.commit()
login_user(user, remember=remember_me)
auth_logger.info(f"API-Login erfolgreich für Benutzer {username}")
user_data = {
"id": user.id,
"username": user.username,
"name": user.name,
"email": user.email,
"is_admin": user.is_admin
}
db_session.close()
return jsonify({
"success": True,
"user": user_data,
"redirect_url": url_for("index")
})
else:
auth_logger.warning(f"Fehlgeschlagener API-Login für Benutzer {username}")
db_session.close()
return jsonify({"error": "Ungültiger Benutzername oder Passwort"}), 401
except Exception as e:
auth_logger.error(f"Fehler beim API-Login: {str(e)}")
return jsonify({"error": "Anmeldefehler. Bitte versuchen Sie es später erneut"}), 500
@app.route("/auth/api/callback", methods=["GET", "POST"])
def api_callback():
"""OAuth-Callback-Endpunkt für externe Authentifizierung"""
try:
# OAuth-Provider bestimmen
provider = request.args.get('provider', 'github')
if request.method == "GET":
# Authorization Code aus URL-Parameter extrahieren
code = request.args.get('code')
state = request.args.get('state')
error = request.args.get('error')
if error:
auth_logger.warning(f"OAuth-Fehler von {provider}: {error}")
return jsonify({
"error": f"OAuth-Authentifizierung fehlgeschlagen: {error}",
"redirect_url": url_for("login")
}), 400
if not code:
auth_logger.warning(f"Kein Authorization Code von {provider} erhalten")
return jsonify({
"error": "Kein Authorization Code erhalten",
"redirect_url": url_for("login")
}), 400
# State-Parameter validieren (CSRF-Schutz)
session_state = session.get('oauth_state')
if not state or state != session_state:
auth_logger.warning(f"Ungültiger State-Parameter von {provider}")
return jsonify({
"error": "Ungültiger State-Parameter",
"redirect_url": url_for("login")
}), 400
# OAuth-Token austauschen
if provider == 'github':
user_data = handle_github_callback(code)
else:
auth_logger.error(f"Unbekannter OAuth-Provider: {provider}")
return jsonify({
"error": "Unbekannter OAuth-Provider",
"redirect_url": url_for("login")
}), 400
if not user_data:
return jsonify({
"error": "Fehler beim Abrufen der Benutzerdaten",
"redirect_url": url_for("login")
}), 400
# Benutzer in Datenbank suchen oder erstellen
db_session = get_db_session()
try:
user = db_session.query(User).filter(
User.email == user_data['email']
).first()
if not user:
# Neuen Benutzer erstellen
user = User(
username=user_data['username'],
email=user_data['email'],
name=user_data['name'],
is_admin=False,
oauth_provider=provider,
oauth_id=str(user_data['id'])
)
# Zufälliges Passwort setzen (wird nicht verwendet)
import secrets
user.set_password(secrets.token_urlsafe(32))
db_session.add(user)
db_session.commit()
auth_logger.info(f"Neuer OAuth-Benutzer erstellt: {user.username} via {provider}")
else:
# Bestehenden Benutzer aktualisieren
user.oauth_provider = provider
user.oauth_id = str(user_data['id'])
user.name = user_data['name']
user.updated_at = datetime.now()
db_session.commit()
auth_logger.info(f"OAuth-Benutzer aktualisiert: {user.username} via {provider}")
# Update last login timestamp
user.update_last_login()
db_session.commit()
login_user(user, remember=True)
# Session-State löschen
session.pop('oauth_state', None)
response_data = {
"success": True,
"user": {
"id": user.id,
"username": user.username,
"name": user.name,
"email": user.email,
"is_admin": user.is_admin
},
"redirect_url": url_for("index")
}
db_session.close()
return jsonify(response_data)
except Exception as e:
db_session.rollback()
db_session.close()
auth_logger.error(f"Datenbankfehler bei OAuth-Callback: {str(e)}")
return jsonify({
"error": "Datenbankfehler bei der Benutzeranmeldung",
"redirect_url": url_for("login")
}), 500
elif request.method == "POST":
# POST-Anfragen für manuelle Token-Übermittlung
data = request.get_json()
if not data:
return jsonify({"error": "Keine Daten erhalten"}), 400
access_token = data.get('access_token')
provider = data.get('provider', 'github')
if not access_token:
return jsonify({"error": "Kein Access Token erhalten"}), 400
# Benutzerdaten mit Access Token abrufen
if provider == 'github':
user_data = get_github_user_data(access_token)
else:
return jsonify({"error": "Unbekannter OAuth-Provider"}), 400
if not user_data:
return jsonify({"error": "Fehler beim Abrufen der Benutzerdaten"}), 400
# Benutzer verarbeiten (gleiche Logik wie bei GET)
db_session = get_db_session()
try:
user = db_session.query(User).filter(
User.email == user_data['email']
).first()
if not user:
user = User(
username=user_data['username'],
email=user_data['email'],
name=user_data['name'],
is_admin=False,
oauth_provider=provider,
oauth_id=str(user_data['id'])
)
import secrets
user.set_password(secrets.token_urlsafe(32))
db_session.add(user)
db_session.commit()
auth_logger.info(f"Neuer OAuth-Benutzer erstellt: {user.username} via {provider}")
else:
user.oauth_provider = provider
user.oauth_id = str(user_data['id'])
user.name = user_data['name']
user.updated_at = datetime.now()
db_session.commit()
auth_logger.info(f"OAuth-Benutzer aktualisiert: {user.username} via {provider}")
# Update last login timestamp
user.update_last_login()
db_session.commit()
login_user(user, remember=True)
response_data = {
"success": True,
"user": {
"id": user.id,
"username": user.username,
"name": user.name,
"email": user.email,
"is_admin": user.is_admin
},
"redirect_url": url_for("index")
}
db_session.close()
return jsonify(response_data)
except Exception as e:
db_session.rollback()
db_session.close()
auth_logger.error(f"Datenbankfehler bei OAuth-Callback: {str(e)}")
return jsonify({
"error": "Datenbankfehler bei der Benutzeranmeldung",
"redirect_url": url_for("login")
}), 500
except Exception as e:
auth_logger.error(f"Fehler im OAuth-Callback: {str(e)}")
return jsonify({
"error": "OAuth-Callback-Fehler",
"redirect_url": url_for("login")
}), 500
def handle_github_callback(code):
"""GitHub OAuth-Callback verarbeiten"""
try:
import requests
# GitHub OAuth-Konfiguration (sollte aus Umgebungsvariablen kommen)
client_id = "7c5d8bef1a5519ec1fdc"
client_secret = "5f1e586204358fbd53cf5fb7d418b3f06ccab8fd"
if not client_id or not client_secret:
auth_logger.error("GitHub OAuth-Konfiguration fehlt")
return None
# Access Token anfordern
token_url = "https://github.com/login/oauth/access_token"
token_data = {
'client_id': client_id,
'client_secret': client_secret,
'code': code
}
token_response = requests.post(
token_url,
data=token_data,
headers={'Accept': 'application/json'},
timeout=10
)
if token_response.status_code != 200:
auth_logger.error(f"GitHub Token-Anfrage fehlgeschlagen: {token_response.status_code}")
return None
token_json = token_response.json()
access_token = token_json.get('access_token')
if not access_token:
auth_logger.error("Kein Access Token von GitHub erhalten")
return None
return get_github_user_data(access_token)
except Exception as e:
auth_logger.error(f"Fehler bei GitHub OAuth-Callback: {str(e)}")
return None
def get_github_user_data(access_token):
"""GitHub-Benutzerdaten mit Access Token abrufen"""
try:
import requests
# Benutzerdaten von GitHub API abrufen
user_url = "https://api.github.com/user"
headers = {
'Authorization': f'token {access_token}',
'Accept': 'application/vnd.github.v3+json'
}
user_response = requests.get(user_url, headers=headers, timeout=10)
if user_response.status_code != 200:
auth_logger.error(f"GitHub User-API-Anfrage fehlgeschlagen: {user_response.status_code}")
return None
user_data = user_response.json()
# E-Mail-Adresse separat abrufen (falls nicht öffentlich)
email = user_data.get('email')
if not email:
email_url = "https://api.github.com/user/emails"
email_response = requests.get(email_url, headers=headers, timeout=10)
if email_response.status_code == 200:
emails = email_response.json()
# Primäre E-Mail-Adresse finden
for email_obj in emails:
if email_obj.get('primary', False):
email = email_obj.get('email')
break
# Fallback: Erste E-Mail-Adresse verwenden
if not email and emails:
email = emails[0].get('email')
if not email:
auth_logger.error("Keine E-Mail-Adresse von GitHub erhalten")
return None
return {
'id': user_data.get('id'),
'username': user_data.get('login'),
'name': user_data.get('name') or user_data.get('login'),
'email': email
}
except Exception as e:
auth_logger.error(f"Fehler beim Abrufen der GitHub-Benutzerdaten: {str(e)}")
return None
# ===== BENUTZER-ROUTEN (ehemals user.py) =====
@app.route("/user/profile", methods=["GET"])
@login_required
def user_profile():
"""Profil-Seite anzeigen"""
user_logger.info(f"Benutzer {current_user.username} hat seine Profilseite aufgerufen")
return render_template("profile.html", user=current_user)
@app.route("/user/settings", methods=["GET"])
@login_required
def user_settings():
"""Einstellungen-Seite anzeigen"""
user_logger.info(f"Benutzer {current_user.username} hat seine Einstellungsseite aufgerufen")
return render_template("settings.html", user=current_user)
@app.route("/user/update-profile", methods=["POST"])
@login_required
def user_update_profile():
"""Benutzerprofilinformationen aktualisieren"""
try:
# Überprüfen, ob es sich um eine JSON-Anfrage handelt
is_json_request = request.is_json or request.headers.get('Content-Type') == 'application/json'
if is_json_request:
data = request.get_json()
name = data.get("name")
email = data.get("email")
department = data.get("department")
position = data.get("position")
phone = data.get("phone")
else:
name = request.form.get("name")
email = request.form.get("email")
department = request.form.get("department")
position = request.form.get("position")
phone = request.form.get("phone")
db_session = get_db_session()
user = db_session.query(User).filter(User.id == int(current_user.id)).first()
if user:
# Aktualisiere die Benutzerinformationen
if name:
user.name = name
if email:
user.email = email
if department:
user.department = department
if position:
user.position = position
if phone:
user.phone = phone
user.updated_at = datetime.now()
db_session.commit()
user_logger.info(f"Benutzer {current_user.username} hat sein Profil aktualisiert")
if is_json_request:
return jsonify({
"success": True,
"message": "Profil erfolgreich aktualisiert"
})
else:
flash("Profil erfolgreich aktualisiert", "success")
return redirect(url_for("user_profile"))
else:
error = "Benutzer nicht gefunden."
if is_json_request:
return jsonify({"error": error}), 404
else:
flash(error, "error")
return redirect(url_for("user_profile"))
except Exception as e:
error = f"Fehler beim Aktualisieren des Profils: {str(e)}"
user_logger.error(error)
if request.is_json:
return jsonify({"error": error}), 500
else:
flash(error, "error")
return redirect(url_for("user_profile"))
finally:
db_session.close()
@app.route("/user/api/update-settings", methods=["POST"])
@login_required
def user_api_update_settings():
"""API-Endpunkt für Einstellungen-Updates (JSON)"""
return user_update_settings()
@app.route("/user/update-settings", methods=["POST"])
@login_required
def user_update_settings():
"""Benutzereinstellungen aktualisieren"""
db_session = get_db_session()
try:
# Überprüfen, ob es sich um eine JSON-Anfrage handelt
is_json_request = request.is_json or request.headers.get('Content-Type') == 'application/json'
# Einstellungen aus der Anfrage extrahieren
if is_json_request:
data = request.get_json()
if not data:
return jsonify({"error": "Keine Daten empfangen"}), 400
theme = data.get("theme", "system")
reduced_motion = bool(data.get("reduced_motion", False))
contrast = data.get("contrast", "normal")
notifications = data.get("notifications", {})
privacy = data.get("privacy", {})
else:
theme = request.form.get("theme", "system")
reduced_motion = request.form.get("reduced_motion") == "on"
contrast = request.form.get("contrast", "normal")
notifications = {
"new_jobs": request.form.get("notify_new_jobs") == "on",
"job_updates": request.form.get("notify_job_updates") == "on",
"system": request.form.get("notify_system") == "on",
"email": request.form.get("notify_email") == "on"
}
privacy = {
"activity_logs": request.form.get("activity_logs") == "on",
"two_factor": request.form.get("two_factor") == "on",
"auto_logout": int(request.form.get("auto_logout", "60"))
}
# Validierung der Eingaben
valid_themes = ["light", "dark", "system"]
if theme not in valid_themes:
theme = "system"
valid_contrasts = ["normal", "high"]
if contrast not in valid_contrasts:
contrast = "normal"
# Benutzer aus der Datenbank laden
user = db_session.query(User).filter(User.id == int(current_user.id)).first()
if not user:
error = "Benutzer nicht gefunden."
if is_json_request:
return jsonify({"error": error}), 404
else:
flash(error, "error")
return redirect(url_for("user_settings"))
# Einstellungen-Dictionary erstellen
settings = {
"theme": theme,
"reduced_motion": reduced_motion,
"contrast": contrast,
"notifications": {
"new_jobs": bool(notifications.get("new_jobs", True)),
"job_updates": bool(notifications.get("job_updates", True)),
"system": bool(notifications.get("system", True)),
"email": bool(notifications.get("email", False))
},
"privacy": {
"activity_logs": bool(privacy.get("activity_logs", True)),
"two_factor": bool(privacy.get("two_factor", False)),
"auto_logout": max(5, min(480, int(privacy.get("auto_logout", 60)))) # 5-480 Minuten
},
"last_updated": datetime.now().isoformat()
}
# Prüfen, ob User-Tabelle eine settings-Spalte hat
if hasattr(user, 'settings'):
# Einstellungen in der Datenbank speichern
import json
user.settings = json.dumps(settings)
else:
# Fallback: In Session speichern (temporär)
session['user_settings'] = settings
user.updated_at = datetime.now()
db_session.commit()
user_logger.info(f"Benutzer {current_user.username} hat seine Einstellungen aktualisiert")
if is_json_request:
return jsonify({
"success": True,
"message": "Einstellungen erfolgreich aktualisiert",
"settings": settings
})
else:
flash("Einstellungen erfolgreich aktualisiert", "success")
return redirect(url_for("user_settings"))
except ValueError as e:
error = f"Ungültige Eingabedaten: {str(e)}"
user_logger.warning(f"Ungültige Einstellungsdaten von Benutzer {current_user.username}: {str(e)}")
if is_json_request:
return jsonify({"error": error}), 400
else:
flash(error, "error")
return redirect(url_for("user_settings"))
except Exception as e:
db_session.rollback()
error = f"Fehler beim Aktualisieren der Einstellungen: {str(e)}"
user_logger.error(f"Fehler beim Aktualisieren der Einstellungen für Benutzer {current_user.username}: {str(e)}")
if is_json_request:
return jsonify({"error": "Interner Serverfehler"}), 500
else:
flash("Fehler beim Speichern der Einstellungen", "error")
return redirect(url_for("user_settings"))
finally:
db_session.close()
@app.route("/api/user/settings", methods=["GET"])
@login_required
def get_user_settings():
"""Holt die aktuellen Benutzereinstellungen"""
try:
# Einstellungen aus Session oder Datenbank laden
user_settings = session.get('user_settings', {})
# Standard-Einstellungen falls keine vorhanden
default_settings = {
"theme": "system",
"reduced_motion": False,
"contrast": "normal",
"notifications": {
"new_jobs": True,
"job_updates": True,
"system": True,
"email": False
},
"privacy": {
"activity_logs": True,
"two_factor": False,
"auto_logout": 60
}
}
# Merge mit Standard-Einstellungen
settings = {**default_settings, **user_settings}
return jsonify({
"success": True,
"settings": settings
})
except Exception as e:
user_logger.error(f"Fehler beim Laden der Benutzereinstellungen: {str(e)}")
return jsonify({
"success": False,
"error": "Fehler beim Laden der Einstellungen"
}), 500
@app.route("/user/change-password", methods=["POST"])
@login_required
def user_change_password():
"""Benutzerpasswort ändern"""
try:
# Überprüfen, ob es sich um eine JSON-Anfrage handelt
is_json_request = request.is_json or request.headers.get('Content-Type') == 'application/json'
if is_json_request:
data = request.get_json()
current_password = data.get("current_password")
new_password = data.get("new_password")
confirm_password = data.get("confirm_password")
else:
current_password = request.form.get("current_password")
new_password = request.form.get("new_password")
confirm_password = request.form.get("confirm_password")
# Prüfen, ob alle Felder ausgefüllt sind
if not current_password or not new_password or not confirm_password:
error = "Alle Passwortfelder müssen ausgefüllt sein."
if is_json_request:
return jsonify({"error": error}), 400
else:
flash(error, "error")
return redirect(url_for("user_profile"))
# Prüfen, ob das neue Passwort und die Bestätigung übereinstimmen
if new_password != confirm_password:
error = "Das neue Passwort und die Bestätigung stimmen nicht überein."
if is_json_request:
return jsonify({"error": error}), 400
else:
flash(error, "error")
return redirect(url_for("user_profile"))
db_session = get_db_session()
user = db_session.query(User).filter(User.id == int(current_user.id)).first()
if user and user.check_password(current_password):
# Passwort aktualisieren
user.set_password(new_password)
user.updated_at = datetime.now()
db_session.commit()
user_logger.info(f"Benutzer {current_user.username} hat sein Passwort geändert")
if is_json_request:
return jsonify({
"success": True,
"message": "Passwort erfolgreich geändert"
})
else:
flash("Passwort erfolgreich geändert", "success")
return redirect(url_for("user_profile"))
else:
error = "Das aktuelle Passwort ist nicht korrekt."
if is_json_request:
return jsonify({"error": error}), 401
else:
flash(error, "error")
return redirect(url_for("user_profile"))
except Exception as e:
error = f"Fehler beim Ändern des Passworts: {str(e)}"
user_logger.error(error)
if request.is_json:
return jsonify({"error": error}), 500
else:
flash(error, "error")
return redirect(url_for("user_profile"))
finally:
db_session.close()
@app.route("/user/export", methods=["GET"])
@login_required
def user_export_data():
"""Exportiert alle Benutzerdaten als JSON für DSGVO-Konformität"""
try:
db_session = get_db_session()
user = db_session.query(User).filter(User.id == int(current_user.id)).first()
if not user:
db_session.close()
return jsonify({"error": "Benutzer nicht gefunden"}), 404
# Benutzerdaten abrufen
user_data = user.to_dict()
# Jobs des Benutzers abrufen
jobs = db_session.query(Job).filter(Job.user_id == user.id).all()
user_data["jobs"] = [job.to_dict() for job in jobs]
# Aktivitäten und Einstellungen hinzufügen
user_data["settings"] = session.get('user_settings', {})
# Persönliche Statistiken
user_data["statistics"] = {
"total_jobs": len(jobs),
"completed_jobs": len([j for j in jobs if j.status == "finished"]),
"failed_jobs": len([j for j in jobs if j.status == "failed"]),
"account_created": user.created_at.isoformat() if user.created_at else None,
"last_login": user.last_login.isoformat() if user.last_login else None
}
db_session.close()
# Daten als JSON-Datei zum Download anbieten
response = make_response(json.dumps(user_data, indent=4))
response.headers["Content-Disposition"] = f"attachment; filename=user_data_{user.username}.json"
response.headers["Content-Type"] = "application/json"
user_logger.info(f"Benutzer {current_user.username} hat seine Daten exportiert")
return response
except Exception as e:
error = f"Fehler beim Exportieren der Benutzerdaten: {str(e)}"
user_logger.error(error)
return jsonify({"error": error}), 500
@app.route("/user/profile", methods=["PUT"])
@login_required
def user_update_profile_api():
"""API-Endpunkt zum Aktualisieren des Benutzerprofils"""
try:
if not request.is_json:
return jsonify({"error": "Anfrage muss im JSON-Format sein"}), 400
data = request.get_json()
db_session = get_db_session()
user = db_session.query(User).filter(User.id == int(current_user.id)).first()
if not user:
db_session.close()
return jsonify({"error": "Benutzer nicht gefunden"}), 404
# Aktualisiere nur die bereitgestellten Felder
if "name" in data:
user.name = data["name"]
if "email" in data:
user.email = data["email"]
if "department" in data:
user.department = data["department"]
if "position" in data:
user.position = data["position"]
if "phone" in data:
user.phone = data["phone"]
if "bio" in data:
user.bio = data["bio"]
user.updated_at = datetime.now()
db_session.commit()
# Aktualisierte Benutzerdaten zurückgeben
user_data = user.to_dict()
db_session.close()
user_logger.info(f"Benutzer {current_user.username} hat sein Profil über die API aktualisiert")
return jsonify({
"success": True,
"message": "Profil erfolgreich aktualisiert",
"user": user_data
})
except Exception as e:
error = f"Fehler beim Aktualisieren des Profils: {str(e)}"
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 =====
@measure_execution_time(logger=printers_logger, task_name="Drucker-Status-Prüfung")
def check_printer_status(ip_address: str, timeout: int = 7) -> Tuple[str, bool]:
"""
Überprüft den Status eines Druckers über Steckdosenabfrage mit Timeout.
Args:
ip_address: IP-Adresse der Drucker-Steckdose
timeout: Timeout in Sekunden (Standard: 7)
Returns:
Tuple[str, bool]: (Status, Aktiv) - Status ist "online" oder "offline", Aktiv ist True/False
"""
if not ip_address or ip_address.strip() == "":
printers_logger.debug(f"Keine IP-Adresse angegeben")
return "offline", False
try:
# IP-Adresse validieren
import ipaddress
try:
ipaddress.ip_address(ip_address.strip())
except ValueError:
printers_logger.warning(f"Ungültige IP-Adresse: {ip_address}")
return "offline", False
# Zuerst prüfen, ob die Steckdose erreichbar ist
if os.name == 'nt': # Windows
cmd = ['ping', '-n', '1', '-w', str(timeout * 1000), ip_address.strip()]
else: # Unix/Linux/macOS
cmd = ['ping', '-c', '1', '-W', str(timeout), ip_address.strip()]
printers_logger.debug(f"Ping-Befehl für {ip_address}: {' '.join(cmd)}")
# Ping ausführen mit Timeout
result = subprocess.run(
cmd,
capture_output=True,
text=True,
encoding='utf-8',
errors='ignore', # Ignoriere Unicode-Fehler
timeout=timeout + 2 # Zusätzlicher Timeout für subprocess
)
# Wenn Steckdose nicht erreichbar ist, ist der Drucker offline
if result.returncode != 0:
printers_logger.debug(f"Ping fehlgeschlagen für {ip_address} (Return Code: {result.returncode})")
return "offline", False
# Jetzt den tatsächlichen Steckdosenstatus abfragen
db_session = get_db_session()
printer = db_session.query(Printer).filter(Printer.plug_ip == ip_address).first()
if not printer:
printers_logger.warning(f"Kein Drucker mit Steckdosen-IP {ip_address} gefunden")
db_session.close()
return "offline", False
# Smart Plug Status prüfen
import requests
from requests.exceptions import RequestException
# Standardwerte aus der Datenbank verwenden
username = printer.plug_username
password = printer.plug_password
try:
# Für TP-Link Smart Plugs oder kompatible Steckdosen
auth = (username, password)
response = requests.get(f"http://{ip_address}/status", auth=auth, timeout=timeout)
if response.status_code == 200:
try:
status_data = response.json()
# Überprüfen ob die Steckdose eingeschaltet ist
if 'system' in status_data and 'get_sysinfo' in status_data['system']:
if status_data['system']['get_sysinfo'].get('relay_state') == 1:
printers_logger.debug(f"Steckdose {ip_address} ist eingeschaltet")
db_session.close()
return "online", True
except (ValueError, KeyError) as e:
printers_logger.debug(f"Fehler beim Parsen der Steckdosen-Antwort: {str(e)}")
# Zweiter Versuch mit einfacher GET-Anfrage
response = requests.get(f"http://{ip_address}", auth=auth, timeout=timeout)
if response.status_code == 200:
printers_logger.debug(f"Steckdose {ip_address} antwortet auf HTTP-Anfrage")
# Wenn wir hier ankommen, ist die Steckdose online, aber wir wissen nicht sicher, ob sie eingeschaltet ist
# Da wir nur die Verfügbarkeit prüfen, nehmen wir an, dass sie aktiv ist, wenn sie antwortet
db_session.close()
return "online", True
except RequestException as e:
printers_logger.debug(f"Fehler bei HTTP-Anfrage an Steckdose {ip_address}: {str(e)}")
# Wenn beide API-Anfragen fehlschlagen, können wir annehmen, dass die Steckdose nicht eingeschaltet ist
db_session.close()
return "offline", False
except subprocess.TimeoutExpired:
printers_logger.warning(f"Ping-Timeout für Drucker {ip_address} nach {timeout} Sekunden")
return "offline", False
except Exception as e:
printers_logger.error(f"Fehler beim Status-Check für Drucker {ip_address}: {str(e)}")
return "offline", False
@measure_execution_time(logger=printers_logger, task_name="Mehrere-Drucker-Status-Prüfung")
def check_multiple_printers_status(printers: List[Dict], timeout: int = 7) -> Dict[int, Tuple[str, bool]]:
"""
Überprüft den Status mehrerer Drucker parallel mit Timeout.
Args:
printers: Liste von Drucker-Dictionaries mit 'id' und 'ip_address'
timeout: Timeout in Sekunden pro Drucker (Standard: 7)
Returns:
Dict[int, Tuple[str, bool]]: Dictionary mit Drucker-ID als Key und (Status, Aktiv) als Value
"""
results = {}
# Parallel-Ausführung mit ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=min(len(printers), 10)) as executor:
# Futures für alle Drucker erstellen
future_to_printer = {
executor.submit(check_printer_status, printer.get('ip_address'), timeout): printer
for printer in printers
}
# Ergebnisse sammeln
for future in as_completed(future_to_printer, timeout=timeout + 2):
printer = future_to_printer[future]
try:
status, active = future.result()
results[printer['id']] = (status, active)
printers_logger.info(f"Drucker {printer['name']} ({printer.get('ip_address')}): {status}")
except Exception as e:
printers_logger.error(f"Fehler bei Status-Check für Drucker {printer['name']}: {str(e)}")
results[printer['id']] = ("offline", False)
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")
@login_required
def admin():
"""Leitet zur neuen Admin-Dashboard-Route weiter."""
if not current_user.is_admin:
flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error")
return redirect(url_for("index"))
return redirect(url_for("admin_page"))
@app.route("/demo")
@login_required
def components_demo():
"""Demo-Seite für UI-Komponenten"""
return render_template("components_demo.html")
@app.route("/printers")
@login_required
def printers_page():
"""Zeigt die Übersichtsseite für Drucker an."""
return render_template("printers.html")
@app.route("/jobs")
@login_required
def jobs_page():
"""Zeigt die Übersichtsseite für Druckaufträge an."""
return render_template("jobs.html")
@app.route("/jobs/new")
@login_required
def new_job_page():
"""Zeigt die Seite zum Erstellen neuer Druckaufträge an."""
return render_template("jobs.html")
@app.route("/stats")
@login_required
def stats_page():
"""Zeigt die Statistik-Seite an."""
return render_template("stats.html")
@app.route("/admin-dashboard")
@login_required
def admin_page():
"""Zeigt die Administrationsseite an."""
if not current_user.is_admin:
flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error")
return redirect(url_for("index"))
# Aktives Tab aus der URL auslesen oder Default-Wert verwenden
active_tab = request.args.get('tab', 'users')
# Daten für das Admin-Panel direkt beim Laden vorbereiten
stats = {}
users = []
printers = []
scheduler_status = {"running": False, "message": "Nicht verfügbar"}
system_info = {"cpu": 0, "memory": 0, "disk": 0}
logs = []
db_session = get_db_session()
try:
# Statistiken laden
from sqlalchemy.orm import joinedload
# Benutzeranzahl
stats["total_users"] = db_session.query(User).count()
# Druckeranzahl und Online-Status
all_printers = db_session.query(Printer).all()
stats["total_printers"] = len(all_printers)
stats["online_printers"] = len([p for p in all_printers if p.status == "online"])
# Aktive Jobs und Warteschlange
stats["active_jobs"] = db_session.query(Job).filter(
Job.status.in_(["printing", "running"])
).count()
stats["queued_jobs"] = db_session.query(Job).filter(
Job.status == "scheduled"
).count()
# Erfolgsrate
total_jobs = db_session.query(Job).filter(
Job.status.in_(["completed", "failed", "cancelled"])
).count()
successful_jobs = db_session.query(Job).filter(
Job.status == "completed"
).count()
if total_jobs > 0:
stats["success_rate"] = int((successful_jobs / total_jobs) * 100)
else:
stats["success_rate"] = 0
# Benutzer laden
if active_tab == 'users':
users = db_session.query(User).all()
users = [user.to_dict() for user in users]
# Drucker laden
if active_tab == 'printers':
printers = db_session.query(Printer).all()
printers = [printer.to_dict() for printer in printers]
# Scheduler-Status laden
if active_tab == 'scheduler':
try:
from utils.scheduler import scheduler_is_running
is_running = scheduler_is_running()
scheduler_status = {
"running": is_running,
"message": "Der Scheduler läuft" if is_running else "Der Scheduler ist gestoppt"
}
except (ImportError, AttributeError):
scheduler_status = {
"running": False,
"message": "Scheduler-Status nicht verfügbar"
}
# System-Informationen laden
if active_tab == 'system':
import os
import psutil
# CPU und Memory
cpu_percent = psutil.cpu_percent(interval=1)
memory = psutil.virtual_memory()
disk = psutil.disk_usage('/')
# Uptime
boot_time = psutil.boot_time()
uptime_seconds = time.time() - boot_time
uptime_days = int(uptime_seconds // 86400)
uptime_hours = int((uptime_seconds % 86400) // 3600)
uptime_minutes = int((uptime_seconds % 3600) // 60)
# Datenbank-Status
db_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'database', 'myp.db')
db_size = 0
if os.path.exists(db_path):
db_size = os.path.getsize(db_path) / (1024 * 1024) # MB
# Scheduler-Status
scheduler_running = False
scheduler_jobs = 0
try:
from utils.job_scheduler import scheduler
scheduler_running = scheduler.running
if hasattr(scheduler, 'get_jobs'):
scheduler_jobs = len(scheduler.get_jobs())
except:
pass
# Nächster Job
next_job = db_session.query(Job).filter(
Job.status == "scheduled"
).order_by(Job.created_at.asc()).first()
next_job_time = "Keine geplanten Jobs"
if next_job:
next_job_time = next_job.created_at.strftime("%d.%m.%Y %H:%M")
system_info = {
"cpu_usage": round(cpu_percent, 1),
"memory_usage": round(memory.percent, 1),
"disk_usage": round((disk.used / disk.total) * 100, 1),
"uptime": f"{uptime_days}d {uptime_hours}h {uptime_minutes}m",
"db_size": f"{db_size:.1f} MB",
"db_connections": "Aktiv",
"scheduler_running": scheduler_running,
"scheduler_jobs": scheduler_jobs,
"next_job": next_job_time
}
# Logs laden
if active_tab == 'logs':
import os
log_level = request.args.get('log_level', 'all')
log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logs')
# Logeinträge sammeln
app_logs = []
for category in ['app', 'auth', 'jobs', 'printers', 'scheduler', 'errors']:
log_file = os.path.join(log_dir, category, f'{category}.log')
if os.path.exists(log_file):
with open(log_file, 'r') as f:
for line in f.readlines()[-100:]: # Nur die letzten 100 Zeilen pro Datei
if log_level != 'all':
if log_level.upper() not in line:
continue
app_logs.append({
'timestamp': line.split(' - ')[0] if ' - ' in line else '',
'level': line.split(' - ')[1].split(' - ')[0] if ' - ' in line and len(line.split(' - ')) > 2 else 'INFO',
'category': category,
'message': ' - '.join(line.split(' - ')[2:]) if ' - ' in line and len(line.split(' - ')) > 2 else line
})
# Nach Zeitstempel sortieren (neueste zuerst)
logs = sorted(app_logs, key=lambda x: x['timestamp'] if x['timestamp'] else '', reverse=True)[:100]
except Exception as e:
app_logger.error(f"Fehler beim Laden der Admin-Daten: {str(e)}")
finally:
db_session.close()
return render_template(
"admin.html",
active_tab=active_tab,
stats=stats,
users=users,
printers=printers,
scheduler_status=scheduler_status,
system_info=system_info,
logs=logs
)
# ===== ERROR MONITORING SYSTEM =====
@app.route("/api/admin/system-health", methods=['GET'])
@login_required
def api_admin_system_health():
"""API-Endpunkt für System-Gesundheitscheck."""
if not current_user.is_admin:
return jsonify({"error": "Berechtigung verweigert"}), 403
db_session = get_db_session()
critical_errors = []
warnings = []
try:
# 1. Datenbank-Schema-Integrität prüfen
try:
# Test verschiedene kritische Tabellen und Spalten
db_session.execute(text("SELECT COUNT(*) FROM guest_requests WHERE duration_minutes IS NOT NULL"))
schema_integrity = "OK"
except Exception as e:
critical_errors.append({
"type": "database_schema",
"message": f"Datenbank-Schema-Fehler: {str(e)}",
"severity": "critical",
"suggested_fix": "Datenbank-Migration ausführen",
"timestamp": datetime.now().isoformat()
})
schema_integrity = "FEHLER"
# 2. Prüfe kritische Spalten in wichtigen Tabellen
schema_checks = [
("guest_requests", "duration_minutes"),
("guest_requests", "file_name"),
("guest_requests", "processed_by"),
("users", "updated_at"),
("jobs", "duration_minutes")
]
missing_columns = []
for table, column in schema_checks:
try:
db_session.execute(text(f"SELECT {column} FROM {table} LIMIT 1"))
except Exception:
missing_columns.append(f"{table}.{column}")
if missing_columns:
critical_errors.append({
"type": "missing_columns",
"message": f"Fehlende Datenbank-Spalten: {', '.join(missing_columns)}",
"severity": "critical",
"suggested_fix": "python utils/database_schema_migration.py ausführen",
"timestamp": datetime.now().isoformat(),
"details": missing_columns
})
# 3. Prüfe auf wiederkehrende Datenbankfehler in den Logs
import os
log_file = os.path.join("logs", "app", f"myp_app_{datetime.now().strftime('%Y_%m_%d')}.log")
recent_db_errors = 0
if os.path.exists(log_file):
try:
with open(log_file, 'r', encoding='utf-8') as f:
last_lines = f.readlines()[-100:] # Letzte 100 Zeilen
for line in last_lines:
if "OperationalError" in line or "no such column" in line:
recent_db_errors += 1
except Exception:
pass
if recent_db_errors > 5:
critical_errors.append({
"type": "frequent_db_errors",
"message": f"{recent_db_errors} Datenbankfehler in letzter Zeit erkannt",
"severity": "high",
"suggested_fix": "System-Logs überprüfen und Migration ausführen",
"timestamp": datetime.now().isoformat()
})
# 4. Prüfe Drucker-Konnektivität
offline_printers = db_session.query(Printer).filter(
Printer.status == "offline",
Printer.active == True
).count()
if offline_printers > 0:
warnings.append({
"type": "printer_offline",
"message": f"{offline_printers} aktive Drucker sind offline",
"severity": "warning",
"suggested_fix": "Drucker-Status überprüfen",
"timestamp": datetime.now().isoformat()
})
# 5. System-Performance Metriken
import psutil
cpu_usage = psutil.cpu_percent(interval=1)
memory_usage = psutil.virtual_memory().percent
disk_usage = psutil.disk_usage('/').percent
if cpu_usage > 90:
warnings.append({
"type": "high_cpu",
"message": f"Hohe CPU-Auslastung: {cpu_usage:.1f}%",
"severity": "warning",
"suggested_fix": "System-Ressourcen überprüfen",
"timestamp": datetime.now().isoformat()
})
if memory_usage > 85:
warnings.append({
"type": "high_memory",
"message": f"Hohe Speicher-Auslastung: {memory_usage:.1f}%",
"severity": "warning",
"suggested_fix": "Speicher-Verbrauch optimieren",
"timestamp": datetime.now().isoformat()
})
# 6. Letzte Migration info
try:
backup_dir = os.path.join("database", "backups")
if os.path.exists(backup_dir):
backup_files = [f for f in os.listdir(backup_dir) if f.endswith('.backup')]
if backup_files:
latest_backup = max(backup_files, key=lambda x: os.path.getctime(os.path.join(backup_dir, x)))
last_migration = latest_backup.replace('.backup', '').replace('myp.db.backup_', '')
else:
last_migration = "Keine Backups gefunden"
else:
last_migration = "Backup-Verzeichnis nicht gefunden"
except Exception:
last_migration = "Unbekannt"
return jsonify({
"success": True,
"health_status": "critical" if critical_errors else ("warning" if warnings else "healthy"),
"critical_errors": critical_errors,
"warnings": warnings,
"schema_integrity": schema_integrity,
"last_migration": last_migration,
"recent_errors_count": recent_db_errors,
"system_metrics": {
"cpu_usage": cpu_usage,
"memory_usage": memory_usage,
"disk_usage": disk_usage
},
"timestamp": datetime.now().isoformat()
})
except Exception as e:
app_logger.error(f"Fehler beim System-Gesundheitscheck: {str(e)}")
return jsonify({
"success": False,
"error": "Fehler beim System-Gesundheitscheck",
"critical_errors": [{
"type": "system_check_failed",
"message": f"System-Check fehlgeschlagen: {str(e)}",
"severity": "critical",
"suggested_fix": "System-Logs überprüfen",
"timestamp": datetime.now().isoformat()
}]
}), 500
finally:
db_session.close()
@app.route("/api/admin/fix-errors", methods=['POST'])
@login_required
@csrf.exempt
def api_admin_fix_errors():
"""API-Endpunkt um automatische Fehler-Reparatur auszuführen."""
if not current_user.is_admin:
return jsonify({"error": "Berechtigung verweigert"}), 403
try:
# Automatische Migration ausführen
import subprocess
import sys
# Migration in separatem Prozess ausführen
result = subprocess.run(
[sys.executable, "utils/database_schema_migration.py"],
cwd=os.path.dirname(os.path.abspath(__file__)),
capture_output=True,
text=True,
timeout=60
)
if result.returncode == 0:
app_logger.info(f"Automatische Migration erfolgreich ausgeführt von Admin {current_user.email}")
return jsonify({
"success": True,
"message": "Automatische Reparatur erfolgreich durchgeführt",
"details": result.stdout
})
else:
app_logger.error(f"Automatische Migration fehlgeschlagen: {result.stderr}")
return jsonify({
"success": False,
"error": "Automatische Reparatur fehlgeschlagen",
"details": result.stderr
}), 500
except subprocess.TimeoutExpired:
return jsonify({
"success": False,
"error": "Migration-Timeout - Vorgang dauerte zu lange"
}), 500
except Exception as e:
app_logger.error(f"Fehler bei automatischer Reparatur: {str(e)}")
return jsonify({
"success": False,
"error": f"Fehler bei automatischer Reparatur: {str(e)}"
}), 500
# Direkter Zugriff auf Logout-Route (für Fallback)
@app.route("/logout", methods=["GET", "POST"])
def logout_redirect():
"""Leitet zur Blueprint-Logout-Route weiter."""
return redirect(url_for("auth_logout"))
# ===== JOB-ROUTEN =====
@app.route("/api/jobs", methods=["GET"])
@login_required
def get_jobs():
db_session = get_db_session()
try:
# Import joinedload for eager loading
from sqlalchemy.orm import joinedload
# Admin sieht alle Jobs, User nur eigene
if current_user.is_admin:
# Eagerly load the user and printer relationships to avoid detached instance errors
jobs = db_session.query(Job).options(joinedload(Job.user), joinedload(Job.printer)).all()
else:
jobs = db_session.query(Job).options(joinedload(Job.user), joinedload(Job.printer)).filter(Job.user_id == int(current_user.id)).all()
# Convert jobs to dictionaries before closing the session
job_dicts = [job.to_dict() for job in jobs]
db_session.close()
return jsonify({
"jobs": job_dicts
})
except Exception as e:
jobs_logger.error(f"Fehler beim Abrufen von Jobs: {str(e)}")
db_session.close()
return jsonify({"error": "Interner Serverfehler"}), 500
@app.route("/api/jobs/<int:job_id>", methods=["GET"])
@login_required
@job_owner_required
def get_job(job_id):
db_session = get_db_session()
try:
from sqlalchemy.orm import joinedload
# 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()
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
@app.route('/api/jobs/check-waiting', methods=['POST'])
@login_required
def check_waiting_jobs():
"""Überprüft wartende Jobs und startet sie, wenn Drucker online gehen."""
try:
db_session = get_db_session()
# Alle wartenden Jobs finden
waiting_jobs = db_session.query(Job).filter(
Job.status == "waiting_for_printer"
).all()
if not waiting_jobs:
db_session.close()
return jsonify({
"message": "Keine wartenden Jobs gefunden",
"updated_jobs": []
})
updated_jobs = []
for job in waiting_jobs:
# Drucker-Status prüfen
printer = db_session.query(Printer).get(job.printer_id)
if printer and printer.plug_ip:
status, active = check_printer_status(printer.plug_ip)
if status == "online" and active:
# Drucker ist jetzt online - Job kann geplant werden
job.status = "scheduled"
updated_jobs.append({
"id": job.id,
"name": job.name,
"printer_name": printer.name,
"status": "scheduled"
})
jobs_logger.info(f"Job {job.id} von 'waiting_for_printer' zu 'scheduled' geändert - Drucker {printer.name} ist online")
if updated_jobs:
db_session.commit()
db_session.close()
return jsonify({
"message": f"{len(updated_jobs)} Jobs aktualisiert",
"updated_jobs": updated_jobs
})
except Exception as e:
jobs_logger.error(f"Fehler beim Überprüfen wartender Jobs: {str(e)}")
return jsonify({"error": "Interner Serverfehler"}), 500
@app.route('/api/jobs/active', methods=['GET'])
@login_required
def get_active_jobs():
"""
Gibt alle aktiven Jobs zurück.
"""
try:
db_session = get_db_session()
from sqlalchemy.orm import joinedload
active_jobs = db_session.query(Job).options(
joinedload(Job.user),
joinedload(Job.printer)
).filter(
Job.status.in_(["scheduled", "running"])
).all()
result = []
for job in active_jobs:
job_dict = job.to_dict()
# Aktuelle Restzeit berechnen
if job.status == "running" and job.end_at:
remaining_time = job.end_at - datetime.now()
if remaining_time.total_seconds() > 0:
job_dict["remaining_minutes"] = int(remaining_time.total_seconds() / 60)
else:
job_dict["remaining_minutes"] = 0
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
@app.route('/api/jobs', methods=['POST'])
@login_required
@measure_execution_time(logger=jobs_logger, task_name="API-Job-Erstellung")
def create_job():
"""
Erstellt einen neuen Job mit dem Status "scheduled".
Body: {
"printer_id": int,
"start_iso": str, # ISO-Datum-String
"duration_minutes": int
}
"""
try:
data = request.json
# Pflichtfelder prüfen
required_fields = ["printer_id", "start_iso", "duration_minutes"]
for field in required_fields:
if field not in data:
return jsonify({"error": f"Feld '{field}' fehlt"}), 400
# Daten extrahieren und validieren
printer_id = int(data["printer_id"])
start_iso = data["start_iso"]
duration_minutes = int(data["duration_minutes"])
# Optional: Jobtitel und Dateipfad
name = data.get("name", f"Druckjob vom {datetime.now().strftime('%d.%m.%Y')}")
file_path = data.get("file_path")
# Start-Zeit parsen
try:
start_at = datetime.fromisoformat(start_iso)
except ValueError:
return jsonify({"error": "Ungültiges Startdatum"}), 400
# Dauer validieren
if duration_minutes <= 0:
return jsonify({"error": "Dauer muss größer als 0 sein"}), 400
# 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.query(Printer).get(printer_id)
if not printer:
db_session.close()
return jsonify({"error": "Drucker nicht gefunden"}), 404
# Prüfen, ob der Drucker online ist
printer_status, printer_active = check_printer_status(printer.plug_ip if printer.plug_ip else "")
# Status basierend auf Drucker-Verfügbarkeit setzen
if printer_status == "online" and printer_active:
job_status = "scheduled"
else:
job_status = "waiting_for_printer"
# Neuen Job erstellen
new_job = Job(
name=name,
printer_id=printer_id,
user_id=current_user.id,
owner_id=current_user.id,
start_at=start_at,
end_at=end_at,
status=job_status,
file_path=file_path,
duration_minutes=duration_minutes
)
db_session.add(new_job)
db_session.commit()
# 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
except Exception as e:
jobs_logger.error(f"Fehler beim Erstellen eines Jobs: {str(e)}")
return jsonify({"error": "Interner Serverfehler", "details": str(e)}), 500
@app.route('/api/jobs/<int:job_id>/extend', methods=['POST'])
@login_required
@job_owner_required
def extend_job(job_id):
"""
Verlängert die Endzeit eines Jobs.
Body: {
"extra_minutes": int
}
"""
try:
data = request.json
# Prüfen, ob die erforderlichen Daten vorhanden sind
if "extra_minutes" not in data:
return jsonify({"error": "Feld 'extra_minutes' fehlt"}), 400
extra_minutes = int(data["extra_minutes"])
# Validieren
if extra_minutes <= 0:
return jsonify({"error": "Zusätzliche Minuten müssen größer als 0 sein"}), 400
db_session = get_db_session()
job = db_session.query(Job).get(job_id)
if not job:
db_session.close()
return jsonify({"error": "Job nicht gefunden"}), 404
# Prüfen, ob der Job verlängert werden kann
if job.status not in ["scheduled", "running"]:
db_session.close()
return jsonify({"error": f"Job kann im Status '{job.status}' nicht verlängert werden"}), 400
# Endzeit aktualisieren
job.end_at = job.end_at + timedelta(minutes=extra_minutes)
job.duration_minutes += extra_minutes
db_session.commit()
# Job-Objekt für die Antwort serialisieren
job_dict = job.to_dict()
db_session.close()
jobs_logger.info(f"Job {job_id} um {extra_minutes} Minuten verlängert, neue Endzeit: {job.end_at}")
return jsonify({"job": job_dict})
except Exception as e:
jobs_logger.error(f"Fehler beim Verlängern von Job {job_id}: {str(e)}")
return jsonify({"error": "Interner Serverfehler", "details": str(e)}), 500
@app.route('/api/jobs/<int:job_id>/finish', methods=['POST'])
@login_required
def finish_job(job_id):
"""
Beendet einen Job manuell und schaltet die Steckdose aus.
Nur für Administratoren erlaubt.
"""
try:
# Prüfen, ob der Benutzer Administrator ist
if not current_user.is_admin:
return jsonify({"error": "Nur Administratoren können Jobs manuell beenden"}), 403
db_session = get_db_session()
job = db_session.query(Job).options(joinedload(Job.printer)).get(job_id)
if not job:
db_session.close()
return jsonify({"error": "Job nicht gefunden"}), 404
# Prüfen, ob der Job beendet werden kann
if job.status not in ["scheduled", "running"]:
db_session.close()
return jsonify({"error": f"Job kann im Status '{job.status}' nicht beendet werden"}), 400
# Steckdose ausschalten
from utils.job_scheduler import toggle_plug
if not toggle_plug(job.printer_id, False):
# Trotzdem weitermachen, aber Warnung loggen
jobs_logger.warning(f"Steckdose für Job {job_id} konnte nicht ausgeschaltet werden")
# Job als beendet markieren
job.status = "finished"
job.actual_end_time = datetime.now()
db_session.commit()
# Job-Objekt für die Antwort serialisieren
job_dict = job.to_dict()
db_session.close()
jobs_logger.info(f"Job {job_id} manuell beendet durch Admin {current_user.id}")
return jsonify({"job": job_dict})
except Exception as e:
jobs_logger.error(f"Fehler beim manuellen Beenden von Job {job_id}: {str(e)}")
return jsonify({"error": "Interner Serverfehler", "details": str(e)}), 500
# ===== DRUCKER-ROUTEN =====
@app.route("/api/printers", methods=["GET"])
@login_required
def get_printers():
"""Gibt alle Drucker zurück - OHNE Status-Check für schnelleres Laden."""
db_session = get_db_session()
try:
# Windows-kompatible Timeout-Implementierung
import threading
import time
printers = None
timeout_occurred = False
def fetch_printers():
nonlocal printers, timeout_occurred
try:
printers = db_session.query(Printer).all()
except Exception as e:
printers_logger.error(f"Datenbankfehler beim Laden der Drucker: {str(e)}")
timeout_occurred = True
# Starte Datenbankabfrage in separatem Thread
thread = threading.Thread(target=fetch_printers)
thread.daemon = True
thread.start()
thread.join(timeout=5) # 5 Sekunden Timeout
if thread.is_alive() or timeout_occurred or printers is None:
printers_logger.warning("Database timeout when fetching printers for basic loading")
return jsonify({
'error': 'Database timeout beim Laden der Drucker',
'timeout': True,
'printers': []
}), 408
# Drucker-Daten OHNE Status-Check zusammenstellen für schnelles Laden
printer_data = []
current_time = datetime.now()
for printer in printers:
printer_data.append({
"id": printer.id,
"name": printer.name,
"model": printer.model or 'Unbekanntes Modell',
"location": printer.location or 'Unbekannter Standort',
"mac_address": printer.mac_address,
"plug_ip": printer.plug_ip,
"status": printer.status or "offline", # Letzter bekannter Status
"active": printer.active if hasattr(printer, 'active') else True,
"ip_address": printer.plug_ip if printer.plug_ip else getattr(printer, 'ip_address', None),
"created_at": printer.created_at.isoformat() if printer.created_at else current_time.isoformat(),
"last_checked": printer.last_checked.isoformat() if hasattr(printer, 'last_checked') and printer.last_checked else None
})
db_session.close()
printers_logger.info(f"Schnelles Laden abgeschlossen: {len(printer_data)} Drucker geladen (ohne Status-Check)")
return jsonify({
"printers": printer_data,
"count": len(printer_data),
"message": "Drucker erfolgreich geladen"
})
except Exception as e:
db_session.rollback()
db_session.close()
printers_logger.error(f"Fehler beim Abrufen der Drucker: {str(e)}")
return jsonify({
"error": f"Fehler beim Laden der Drucker: {str(e)}",
"printers": []
}), 500
@app.route("/api/printers/status", methods=["GET"])
@login_required
@measure_execution_time(logger=printers_logger, task_name="API-Drucker-Status-Abfrage")
def get_printers_with_status():
"""Gibt alle Drucker MIT aktuellem Status-Check zurück - für Aktualisierung."""
db_session = get_db_session()
try:
# Windows-kompatible Timeout-Implementierung
import threading
import time
printers = None
timeout_occurred = False
def fetch_printers():
nonlocal printers, timeout_occurred
try:
printers = db_session.query(Printer).all()
except Exception as e:
printers_logger.error(f"Datenbankfehler beim Status-Check: {str(e)}")
timeout_occurred = True
# Starte Datenbankabfrage in separatem Thread
thread = threading.Thread(target=fetch_printers)
thread.daemon = True
thread.start()
thread.join(timeout=8) # 8 Sekunden Timeout für Status-Check
if thread.is_alive() or timeout_occurred or printers is None:
printers_logger.warning("Database timeout when fetching printers for status check")
return jsonify({
'error': 'Database timeout beim Status-Check der Drucker',
'timeout': True
}), 408
# Drucker-Daten für Status-Check vorbereiten
printer_data = []
for printer in printers:
# Verwende plug_ip als primäre IP-Adresse, fallback auf ip_address
ip_to_check = printer.plug_ip if printer.plug_ip else getattr(printer, 'ip_address', None)
printer_data.append({
'id': printer.id,
'name': printer.name,
'ip_address': ip_to_check,
'location': printer.location,
'model': printer.model
})
# Status aller Drucker parallel überprüfen mit 7-Sekunden-Timeout
printers_logger.info(f"Starte Status-Check für {len(printer_data)} Drucker mit 7-Sekunden-Timeout")
# Fallback: Wenn keine IP-Adressen vorhanden sind, alle als offline markieren
if not any(p['ip_address'] for p in printer_data):
printers_logger.warning("Keine IP-Adressen für Drucker gefunden - alle als offline markiert")
status_results = {p['id']: ("offline", False) for p in printer_data}
else:
try:
status_results = check_multiple_printers_status(printer_data, timeout=7)
except Exception as e:
printers_logger.error(f"Fehler beim Status-Check: {str(e)}")
# Fallback: alle als offline markieren
status_results = {p['id']: ("offline", False) for p in printer_data}
# Ergebnisse zusammenstellen und Datenbank aktualisieren
status_data = []
current_time = datetime.now()
for printer in printers:
if printer.id in status_results:
status, active = status_results[printer.id]
# Mapping für Frontend-Kompatibilität
if status == "online":
frontend_status = "available"
else:
frontend_status = "offline"
else:
# Fallback falls kein Ergebnis vorliegt
frontend_status = "offline"
active = False
# Status in der Datenbank aktualisieren
printer.status = frontend_status
printer.active = active
# Setze last_checked falls das Feld existiert
if hasattr(printer, 'last_checked'):
printer.last_checked = current_time
status_data.append({
"id": printer.id,
"name": printer.name,
"model": printer.model or 'Unbekanntes Modell',
"location": printer.location or 'Unbekannter Standort',
"mac_address": printer.mac_address,
"plug_ip": printer.plug_ip,
"status": frontend_status,
"active": active,
"ip_address": printer.plug_ip if printer.plug_ip else getattr(printer, 'ip_address', None),
"created_at": printer.created_at.isoformat() if printer.created_at else current_time.isoformat(),
"last_checked": current_time.isoformat()
})
# Speichere die aktualisierten Status
try:
db_session.commit()
printers_logger.info("Drucker-Status erfolgreich in Datenbank aktualisiert")
except Exception as e:
printers_logger.warning(f"Fehler beim Speichern der Status-Updates: {str(e)}")
# Nicht kritisch, Status-Check kann trotzdem zurückgegeben werden
db_session.close()
online_count = len([s for s in status_data if s['status'] == 'available'])
printers_logger.info(f"Status-Check abgeschlossen: {online_count} von {len(status_data)} Drucker online")
return jsonify(status_data)
except Exception as e:
db_session.rollback()
db_session.close()
printers_logger.error(f"Fehler beim Status-Check der Drucker: {str(e)}")
return jsonify({
"error": f"Fehler beim Status-Check: {str(e)}",
"printers": []
}), 500
@app.route("/api/jobs/current", methods=["GET"])
@login_required
def get_current_job():
"""Gibt den aktuellen Job des Benutzers zurück."""
db_session = get_db_session()
try:
current_job = db_session.query(Job).filter(
Job.user_id == int(current_user.id),
Job.status.in_(["scheduled", "running"])
).order_by(Job.start_at).first()
if current_job:
job_data = current_job.to_dict()
else:
job_data = None
db_session.close()
return jsonify(job_data)
except Exception as e:
db_session.close()
return jsonify({"error": str(e)}), 500
# ===== WEITERE API-ROUTEN =====
@app.route("/api/printers/<int:printer_id>", methods=["GET"])
@login_required
def get_printer(printer_id):
"""Gibt einen spezifischen Drucker zurück."""
db_session = get_db_session()
try:
printer = db_session.query(Printer).get(printer_id)
if not printer:
db_session.close()
return jsonify({"error": "Drucker nicht gefunden"}), 404
# Status-Check für diesen Drucker
ip_to_check = printer.plug_ip if printer.plug_ip else getattr(printer, 'ip_address', None)
if ip_to_check:
status, active = check_printer_status(ip_to_check)
printer.status = "available" if status == "online" else "offline"
printer.active = active
db_session.commit()
printer_data = {
"id": printer.id,
"name": printer.name,
"model": printer.model or 'Unbekanntes Modell',
"location": printer.location or 'Unbekannter Standort',
"mac_address": printer.mac_address,
"plug_ip": printer.plug_ip,
"status": printer.status or "offline",
"active": printer.active if hasattr(printer, 'active') else True,
"ip_address": ip_to_check,
"created_at": printer.created_at.isoformat() if printer.created_at else datetime.now().isoformat()
}
db_session.close()
return jsonify(printer_data)
except Exception as e:
db_session.close()
printers_logger.error(f"Fehler beim Abrufen des Druckers {printer_id}: {str(e)}")
return jsonify({"error": "Interner Serverfehler"}), 500
@app.route("/api/printers", methods=["POST"])
@login_required
def create_printer():
"""Erstellt einen neuen Drucker (nur für Admins)."""
if not current_user.is_admin:
return jsonify({"error": "Nur Administratoren können Drucker erstellen"}), 403
try:
data = request.json
# Pflichtfelder prüfen
required_fields = ["name", "plug_ip"]
for field in required_fields:
if field not in data:
return jsonify({"error": f"Feld '{field}' fehlt"}), 400
db_session = get_db_session()
# Prüfen, ob bereits ein Drucker mit diesem Namen existiert
existing_printer = db_session.query(Printer).filter(Printer.name == data["name"]).first()
if existing_printer:
db_session.close()
return jsonify({"error": "Ein Drucker mit diesem Namen existiert bereits"}), 400
# Neuen Drucker erstellen
new_printer = Printer(
name=data["name"],
model=data.get("model", ""),
location=data.get("location", ""),
mac_address=data.get("mac_address", ""),
plug_ip=data["plug_ip"],
status="offline",
active=True, # Neue Drucker sind standardmäßig aktiv
created_at=datetime.now()
)
db_session.add(new_printer)
db_session.commit()
# Sofortiger Status-Check für den neuen Drucker
ip_to_check = new_printer.plug_ip
if ip_to_check:
status, active = check_printer_status(ip_to_check)
new_printer.status = "available" if status == "online" else "offline"
new_printer.active = active
db_session.commit()
printer_data = {
"id": new_printer.id,
"name": new_printer.name,
"model": new_printer.model,
"location": new_printer.location,
"mac_address": new_printer.mac_address,
"plug_ip": new_printer.plug_ip,
"status": new_printer.status,
"active": new_printer.active,
"created_at": new_printer.created_at.isoformat()
}
db_session.close()
printers_logger.info(f"Neuer Drucker '{new_printer.name}' erstellt von Admin {current_user.id}")
return jsonify({"printer": printer_data, "message": "Drucker erfolgreich erstellt"}), 201
except Exception as e:
printers_logger.error(f"Fehler beim Erstellen eines Druckers: {str(e)}")
return jsonify({"error": "Interner Serverfehler"}), 500
@app.route("/api/printers/add", methods=["POST"])
@login_required
def add_printer():
"""Alternativer Endpunkt zum Hinzufügen von Druckern (für Frontend-Kompatibilität)."""
return create_printer()
@app.route("/api/printers/<int:printer_id>", methods=["PUT"])
@login_required
def update_printer(printer_id):
"""Aktualisiert einen Drucker (nur für Admins)."""
if not current_user.is_admin:
return jsonify({"error": "Nur Administratoren können Drucker bearbeiten"}), 403
try:
data = request.json
db_session = get_db_session()
printer = db_session.query(Printer).get(printer_id)
if not printer:
db_session.close()
return jsonify({"error": "Drucker nicht gefunden"}), 404
# Aktualisierbare Felder
updatable_fields = ["name", "model", "location", "mac_address", "plug_ip"]
for field in updatable_fields:
if field in data:
setattr(printer, field, data[field])
db_session.commit()
printer_data = {
"id": printer.id,
"name": printer.name,
"model": printer.model,
"location": printer.location,
"mac_address": printer.mac_address,
"plug_ip": printer.plug_ip,
"status": printer.status,
"created_at": printer.created_at.isoformat() if printer.created_at else datetime.now().isoformat()
}
db_session.close()
printers_logger.info(f"Drucker {printer_id} aktualisiert von Admin {current_user.id}")
return jsonify({"printer": printer_data})
except Exception as e:
printers_logger.error(f"Fehler beim Aktualisieren des Druckers {printer_id}: {str(e)}")
return jsonify({"error": "Interner Serverfehler"}), 500
@app.route("/api/printers/<int:printer_id>", methods=["DELETE"])
@login_required
def delete_printer(printer_id):
"""Löscht einen Drucker (nur für Admins)."""
if not current_user.is_admin:
return jsonify({"error": "Nur Administratoren können Drucker löschen"}), 403
try:
db_session = get_db_session()
printer = db_session.query(Printer).get(printer_id)
if not printer:
db_session.close()
return jsonify({"error": "Drucker nicht gefunden"}), 404
# Prüfen, ob noch aktive Jobs für diesen Drucker existieren
active_jobs = db_session.query(Job).filter(
Job.printer_id == printer_id,
Job.status.in_(["scheduled", "running"])
).count()
if active_jobs > 0:
db_session.close()
return jsonify({"error": f"Drucker kann nicht gelöscht werden: {active_jobs} aktive Jobs vorhanden"}), 400
printer_name = printer.name
db_session.delete(printer)
db_session.commit()
db_session.close()
printers_logger.info(f"Drucker '{printer_name}' (ID: {printer_id}) gelöscht von Admin {current_user.id}")
return jsonify({"message": "Drucker erfolgreich gelöscht"})
except Exception as e:
printers_logger.error(f"Fehler beim Löschen des Druckers {printer_id}: {str(e)}")
return jsonify({"error": "Interner Serverfehler"}), 500
@app.route("/api/jobs/<int:job_id>", methods=["DELETE"])
@login_required
@job_owner_required
def delete_job(job_id):
"""Löscht einen Job."""
try:
db_session = get_db_session()
job = db_session.query(Job).get(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({"message": "Job erfolgreich gelöscht"})
except Exception as e:
jobs_logger.error(f"Fehler beim Löschen des Jobs {job_id}: {str(e)}")
return jsonify({"error": "Interner Serverfehler"}), 500
@app.route("/api/jobs/<int:job_id>/cancel", methods=["POST"])
@login_required
@job_owner_required
def cancel_job(job_id):
"""Bricht einen Job ab."""
try:
db_session = get_db_session()
job = db_session.query(Job).get(job_id)
if not job:
db_session.close()
return jsonify({"error": "Job nicht gefunden"}), 404
# Prüfen, ob der Job abgebrochen werden kann
if job.status not in ["scheduled", "running"]:
db_session.close()
return jsonify({"error": f"Job kann im Status '{job.status}' nicht abgebrochen werden"}), 400
# Job als abgebrochen markieren
job.status = "cancelled"
job.actual_end_time = datetime.now()
# Wenn der Job läuft, Steckdose ausschalten
if job.status == "running":
from utils.job_scheduler import toggle_plug
toggle_plug(job.printer_id, False)
db_session.commit()
job_dict = job.to_dict()
db_session.close()
jobs_logger.info(f"Job {job_id} abgebrochen von Benutzer {current_user.id}")
return jsonify({"job": job_dict})
except Exception as e:
jobs_logger.error(f"Fehler beim Abbrechen des Jobs {job_id}: {str(e)}")
return jsonify({"error": "Interner Serverfehler"}), 500
@app.route("/api/stats", methods=["GET"])
@login_required
def get_stats():
"""Gibt Statistiken zurück."""
try:
db_session = get_db_session()
# Grundlegende Statistiken
total_users = db_session.query(User).count()
total_printers = db_session.query(Printer).count()
total_jobs = db_session.query(Job).count()
# Jobs nach Status
completed_jobs = db_session.query(Job).filter(Job.status == "completed").count()
failed_jobs = db_session.query(Job).filter(Job.status == "failed").count()
cancelled_jobs = db_session.query(Job).filter(Job.status == "cancelled").count()
active_jobs = db_session.query(Job).filter(Job.status.in_(["scheduled", "running"])).count()
# Online-Drucker
online_printers = db_session.query(Printer).filter(Printer.status == "available").count()
# Erfolgsrate
finished_jobs = completed_jobs + failed_jobs + cancelled_jobs
success_rate = (completed_jobs / finished_jobs * 100) if finished_jobs > 0 else 0
# Benutzer-spezifische Statistiken (falls nicht Admin)
user_stats = {}
if not current_user.is_admin:
user_jobs = db_session.query(Job).filter(Job.user_id == int(current_user.id)).count()
user_completed = db_session.query(Job).filter(
Job.user_id == int(current_user.id),
Job.status == "completed"
).count()
user_stats = {
"total_jobs": user_jobs,
"completed_jobs": user_completed,
"success_rate": (user_completed / user_jobs * 100) if user_jobs > 0 else 0
}
db_session.close()
stats = {
"total_users": total_users,
"total_printers": total_printers,
"online_printers": online_printers,
"total_jobs": total_jobs,
"completed_jobs": completed_jobs,
"failed_jobs": failed_jobs,
"cancelled_jobs": cancelled_jobs,
"active_jobs": active_jobs,
"success_rate": round(success_rate, 1),
"user_stats": user_stats
}
return jsonify(stats)
except Exception as e:
app_logger.error(f"Fehler beim Abrufen der Statistiken: {str(e)}")
return jsonify({"error": "Interner Serverfehler"}), 500
@app.route("/api/admin/users", methods=["GET"])
@login_required
def get_users():
"""Gibt alle Benutzer zurück (nur für Admins)."""
if not current_user.is_admin:
return jsonify({"error": "Nur Administratoren können Benutzer anzeigen"}), 403
try:
db_session = get_db_session()
users = db_session.query(User).all()
user_data = []
for user in users:
user_data.append({
"id": user.id,
"username": user.username,
"email": user.email,
"first_name": user.first_name,
"last_name": user.last_name,
"is_admin": user.is_admin,
"created_at": user.created_at.isoformat() if user.created_at else None,
"last_login": user.last_login.isoformat() if hasattr(user, 'last_login') and user.last_login else None
})
db_session.close()
return jsonify({"users": user_data})
except Exception as e:
app_logger.error(f"Fehler beim Abrufen der Benutzer: {str(e)}")
return jsonify({"error": "Interner Serverfehler"}), 500
@app.route("/api/admin/users/<int:user_id>", methods=["PUT"])
@login_required
def update_user(user_id):
"""Aktualisiert einen Benutzer (nur für Admins)."""
if not current_user.is_admin:
return jsonify({"error": "Nur Administratoren können Benutzer bearbeiten"}), 403
try:
data = request.json
db_session = get_db_session()
user = db_session.get(User, user_id)
if not user:
db_session.close()
return jsonify({"error": "Benutzer nicht gefunden"}), 404
# Aktualisierbare Felder
updatable_fields = ["username", "email", "first_name", "last_name", "is_admin"]
for field in updatable_fields:
if field in data:
setattr(user, field, data[field])
# Passwort separat behandeln
if "password" in data and data["password"]:
user.set_password(data["password"])
db_session.commit()
user_data = {
"id": user.id,
"username": user.username,
"email": user.email,
"first_name": user.first_name,
"last_name": user.last_name,
"is_admin": user.is_admin,
"created_at": user.created_at.isoformat() if user.created_at else None
}
db_session.close()
user_logger.info(f"Benutzer {user_id} aktualisiert von Admin {current_user.id}")
return jsonify({"user": user_data})
except Exception as e:
user_logger.error(f"Fehler beim Aktualisieren des Benutzers {user_id}: {str(e)}")
return jsonify({"error": "Interner Serverfehler"}), 500
@app.route("/api/admin/users/<int:user_id>", methods=["DELETE"])
@login_required
def delete_user(user_id):
"""Löscht einen Benutzer (nur für Admins)."""
if not current_user.is_admin:
return jsonify({"error": "Nur Administratoren können Benutzer löschen"}), 403
# Verhindern, dass sich der Admin selbst löscht
if user_id == current_user.id:
return jsonify({"error": "Sie können sich nicht selbst löschen"}), 400
try:
db_session = get_db_session()
user = db_session.get(User, user_id)
if not user:
db_session.close()
return jsonify({"error": "Benutzer nicht gefunden"}), 404
# Prüfen, ob noch aktive Jobs für diesen Benutzer existieren
active_jobs = db_session.query(Job).filter(
Job.user_id == user_id,
Job.status.in_(["scheduled", "running"])
).count()
if active_jobs > 0:
db_session.close()
return jsonify({"error": f"Benutzer kann nicht gelöscht werden: {active_jobs} aktive Jobs vorhanden"}), 400
username = user.username
db_session.delete(user)
db_session.commit()
db_session.close()
user_logger.info(f"Benutzer '{username}' (ID: {user_id}) gelöscht von Admin {current_user.id}")
return jsonify({"message": "Benutzer erfolgreich gelöscht"})
except Exception as e:
user_logger.error(f"Fehler beim Löschen des Benutzers {user_id}: {str(e)}")
return jsonify({"error": "Interner Serverfehler"}), 500
# ===== FEHLERBEHANDLUNG =====
@app.errorhandler(404)
def not_found_error(error):
return render_template('errors/404.html'), 404
@app.errorhandler(500)
def internal_error(error):
return render_template('errors/500.html'), 500
@app.errorhandler(403)
def forbidden_error(error):
return render_template('errors/403.html'), 403
# ===== ADMIN - DATENBANK-VERWALTUNG =====
@app.route('/api/admin/database/stats', methods=['GET'])
@admin_required
def get_database_stats():
"""Gibt Datenbank-Statistiken zurück."""
try:
if database_monitor is None:
return jsonify({
"success": False,
"error": "Database Monitor nicht verfügbar"
}), 503
stats = database_monitor.get_database_stats()
return jsonify({
"success": True,
"stats": stats
})
except Exception as e:
app_logger.error(f"Fehler beim Abrufen der Datenbank-Statistiken: {str(e)}")
return jsonify({
"success": False,
"error": str(e)
}), 500
@app.route('/api/admin/database/health', methods=['GET'])
@admin_required
def check_database_health():
"""Führt eine Datenbank-Gesundheitsprüfung durch."""
try:
if database_monitor is None:
return jsonify({
"success": False,
"error": "Database Monitor nicht verfügbar"
}), 503
health = database_monitor.check_database_health()
return jsonify({
"success": True,
"health": health
})
except Exception as e:
app_logger.error(f"Fehler bei Datenbank-Gesundheitsprüfung: {str(e)}")
return jsonify({
"success": False,
"error": str(e)
}), 500
@app.route('/api/admin/database/optimize', methods=['POST'])
@admin_required
def optimize_database():
"""Führt Datenbank-Optimierung durch."""
try:
if database_monitor is None:
return jsonify({
"success": False,
"error": "Database Monitor nicht verfügbar"
}), 503
result = database_monitor.optimize_database()
return jsonify({
"success": result["success"],
"result": result
})
except Exception as e:
app_logger.error(f"Fehler bei Datenbank-Optimierung: {str(e)}")
return jsonify({
"success": False,
"error": str(e)
}), 500
@app.route('/api/admin/database/backup', methods=['POST'])
@admin_required
def create_database_backup():
"""Erstellt ein manuelles Datenbank-Backup."""
try:
if backup_manager is None:
return jsonify({
"success": False,
"error": "Backup Manager nicht verfügbar"
}), 503
data = request.get_json() or {}
compress = data.get('compress', True)
backup_path = backup_manager.create_backup(compress=compress)
return jsonify({
"success": True,
"backup_path": backup_path,
"message": "Backup erfolgreich erstellt"
})
except Exception as e:
app_logger.error(f"Fehler beim Erstellen des Backups: {str(e)}")
return jsonify({
"success": False,
"error": str(e)
}), 500
@app.route('/api/admin/database/backups', methods=['GET'])
@admin_required
def list_database_backups():
"""Listet alle verfügbaren Datenbank-Backups auf."""
try:
if backup_manager is None:
return jsonify({
"success": False,
"error": "Backup Manager nicht verfügbar"
}), 503
backups = backup_manager.get_backup_list()
# Konvertiere datetime-Objekte zu Strings für JSON
for backup in backups:
backup['created'] = backup['created'].isoformat()
return jsonify({
"success": True,
"backups": backups
})
except Exception as e:
app_logger.error(f"Fehler beim Abrufen der Backup-Liste: {str(e)}")
return jsonify({
"success": False,
"error": str(e)
}), 500
@app.route('/api/admin/database/backup/restore', methods=['POST'])
@admin_required
def restore_database_backup():
"""Stellt ein Datenbank-Backup wieder her."""
try:
if backup_manager is None:
return jsonify({
"success": False,
"error": "Backup Manager nicht verfügbar"
}), 503
data = request.get_json()
if not data or 'backup_path' not in data:
return jsonify({
"success": False,
"error": "Backup-Pfad erforderlich"
}), 400
backup_path = data['backup_path']
# Sicherheitsprüfung: Nur Backups aus dem Backup-Verzeichnis erlauben
if not backup_path.startswith(backup_manager.backup_dir):
return jsonify({
"success": False,
"error": "Ungültiger Backup-Pfad"
}), 400
success = backup_manager.restore_backup(backup_path)
if success:
return jsonify({
"success": True,
"message": "Backup erfolgreich wiederhergestellt"
})
else:
return jsonify({
"success": False,
"error": "Fehler beim Wiederherstellen des Backups"
}), 500
except Exception as e:
app_logger.error(f"Fehler beim Wiederherstellen des Backups: {str(e)}")
return jsonify({
"success": False,
"error": str(e)
}), 500
@app.route('/api/admin/database/backup/cleanup', methods=['POST'])
@admin_required
def cleanup_old_backups():
"""Löscht alte Datenbank-Backups."""
try:
backup_dir = os.path.join(os.path.dirname(__file__), 'database', 'backups')
if not os.path.exists(backup_dir):
return jsonify({"error": "Backup-Verzeichnis nicht gefunden"}), 404
# Backups älter als 30 Tage löschen
cutoff_date = datetime.now() - timedelta(days=30)
deleted_count = 0
for filename in os.listdir(backup_dir):
if filename.endswith('.sql'):
file_path = os.path.join(backup_dir, filename)
file_mtime = datetime.fromtimestamp(os.path.getmtime(file_path))
if file_mtime < cutoff_date:
os.remove(file_path)
deleted_count += 1
return jsonify({
"message": f"{deleted_count} alte Backups gelöscht",
"deleted_count": deleted_count
})
except Exception as e:
app_logger.error(f"Fehler beim Löschen alter Backups: {str(e)}")
return jsonify({"error": "Interner Serverfehler"}), 500
@app.route('/api/admin/stats/live', methods=['GET'])
@admin_required
def get_admin_live_stats():
"""Liefert Live-Statistiken für das Admin-Dashboard."""
try:
db_session = get_db_session()
# Aktuelle Statistiken sammeln
total_users = db_session.query(User).count()
total_printers = db_session.query(Printer).count()
total_jobs = db_session.query(Job).count()
active_jobs = db_session.query(Job).filter(Job.status.in_(["scheduled", "running"])).count()
# Printer-Status
available_printers = db_session.query(Printer).filter(Printer.status == "available").count()
offline_printers = db_session.query(Printer).filter(Printer.status == "offline").count()
maintenance_printers = db_session.query(Printer).filter(Printer.status == "maintenance").count()
# Jobs heute
today = datetime.now().date()
jobs_today = db_session.query(Job).filter(
func.date(Job.created_at) == today
).count()
# Erfolgreiche Jobs heute
completed_jobs_today = db_session.query(Job).filter(
func.date(Job.created_at) == today,
Job.status == "completed"
).count()
db_session.close()
stats = {
"users": {
"total": total_users
},
"printers": {
"total": total_printers,
"available": available_printers,
"offline": offline_printers,
"maintenance": maintenance_printers
},
"jobs": {
"total": total_jobs,
"active": active_jobs,
"today": jobs_today,
"completed_today": completed_jobs_today
},
"timestamp": datetime.now().isoformat()
}
return jsonify(stats)
except Exception as e:
app_logger.error(f"Fehler beim Abrufen der Live-Statistiken: {str(e)}")
return jsonify({"error": "Interner Serverfehler"}), 500
@app.route('/api/admin/system/status', methods=['GET'])
@admin_required
def get_system_status():
"""Liefert System-Status-Informationen."""
try:
import psutil
import platform
# CPU und Memory
cpu_percent = psutil.cpu_percent(interval=1)
memory = psutil.virtual_memory()
disk = psutil.disk_usage('/')
# Netzwerk (vereinfacht)
network = psutil.net_io_counters()
system_info = {
"platform": platform.system(),
"platform_release": platform.release(),
"platform_version": platform.version(),
"machine": platform.machine(),
"processor": platform.processor(),
"cpu": {
"percent": cpu_percent,
"count": psutil.cpu_count()
},
"memory": {
"total": memory.total,
"available": memory.available,
"percent": memory.percent,
"used": memory.used
},
"disk": {
"total": disk.total,
"used": disk.used,
"free": disk.free,
"percent": (disk.used / disk.total) * 100
},
"network": {
"bytes_sent": network.bytes_sent,
"bytes_recv": network.bytes_recv
},
"timestamp": datetime.now().isoformat()
}
return jsonify(system_info)
except ImportError:
return jsonify({
"error": "psutil nicht installiert",
"message": "Systemstatus kann nicht abgerufen werden"
}), 500
except Exception as e:
app_logger.error(f"Fehler beim Abrufen des Systemstatus: {str(e)}")
return jsonify({"error": "Interner Serverfehler"}), 500
@app.route('/api/admin/database/status', methods=['GET'])
@admin_required
def get_database_status():
"""Liefert Datenbank-Status-Informationen."""
try:
db_session = get_db_session()
# Tabellen-Informationen sammeln
table_stats = {}
# User-Tabelle
user_count = db_session.query(User).count()
latest_user = db_session.query(User).order_by(User.created_at.desc()).first()
# Printer-Tabelle
printer_count = db_session.query(Printer).count()
latest_printer = db_session.query(Printer).order_by(Printer.created_at.desc()).first()
# Job-Tabelle
job_count = db_session.query(Job).count()
latest_job = db_session.query(Job).order_by(Job.created_at.desc()).first()
table_stats = {
"users": {
"count": user_count,
"latest": latest_user.created_at.isoformat() if latest_user else None
},
"printers": {
"count": printer_count,
"latest": latest_printer.created_at.isoformat() if latest_printer else None
},
"jobs": {
"count": job_count,
"latest": latest_job.created_at.isoformat() if latest_job else None
}
}
db_session.close()
# Datenbank-Dateigröße (falls SQLite)
db_file_size = None
try:
db_path = os.path.join(os.path.dirname(__file__), 'database', 'app.db')
if os.path.exists(db_path):
db_file_size = os.path.getsize(db_path)
except:
pass
status = {
"tables": table_stats,
"database_size": db_file_size,
"timestamp": datetime.now().isoformat(),
"connection_status": "connected"
}
return jsonify(status)
except Exception as e:
app_logger.error(f"Fehler beim Abrufen des Datenbankstatus: {str(e)}")
return jsonify({
"error": "Datenbankfehler",
"connection_status": "error",
"timestamp": datetime.now().isoformat()
}), 500
# ===== WEITERE UI-ROUTEN =====
@app.route("/terms")
def terms():
"""Zeigt die Nutzungsbedingungen an."""
return render_template("terms.html")
@app.route("/privacy")
def privacy():
"""Zeigt die Datenschutzerklärung an."""
return render_template("privacy.html")
@app.route("/admin/users/add")
@login_required
def admin_add_user_page():
"""Zeigt die Seite zum Hinzufügen eines neuen Benutzers an."""
if not current_user.is_admin:
flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error")
return redirect(url_for("index"))
return render_template("admin_add_user.html")
@app.route("/admin/printers/add")
@login_required
def admin_add_printer_page():
"""Zeigt die Seite zum Hinzufügen eines neuen Druckers an."""
if not current_user.is_admin:
flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error")
return redirect(url_for("index"))
return render_template("admin_add_printer.html")
@app.route("/admin/printers/<int:printer_id>/manage")
@login_required
def admin_manage_printer_page(printer_id):
"""Zeigt die Drucker-Verwaltungsseite an."""
if not current_user.is_admin:
flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error")
return redirect(url_for("index"))
db_session = get_db_session()
try:
printer = db_session.get(Printer, printer_id)
if not printer:
flash("Drucker nicht gefunden.", "error")
return redirect(url_for("admin_page"))
printer_data = {
"id": printer.id,
"name": printer.name,
"model": printer.model or 'Unbekanntes Modell',
"location": printer.location or 'Unbekannter Standort',
"mac_address": printer.mac_address,
"plug_ip": printer.plug_ip,
"status": printer.status or "offline",
"active": printer.active if hasattr(printer, 'active') else True,
"created_at": printer.created_at.isoformat() if printer.created_at else datetime.now().isoformat()
}
db_session.close()
return render_template("admin_manage_printer.html", printer=printer_data)
except Exception as e:
db_session.close()
app_logger.error(f"Fehler beim Laden der Drucker-Verwaltung: {str(e)}")
flash("Fehler beim Laden der Drucker-Daten.", "error")
return redirect(url_for("admin_page"))
@app.route("/admin/printers/<int:printer_id>/settings")
@login_required
def admin_printer_settings_page(printer_id):
"""Zeigt die Drucker-Einstellungsseite an."""
if not current_user.is_admin:
flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error")
return redirect(url_for("index"))
db_session = get_db_session()
try:
printer = db_session.get(Printer, printer_id)
if not printer:
flash("Drucker nicht gefunden.", "error")
return redirect(url_for("admin_page"))
printer_data = {
"id": printer.id,
"name": printer.name,
"model": printer.model or 'Unbekanntes Modell',
"location": printer.location or 'Unbekannter Standort',
"mac_address": printer.mac_address,
"plug_ip": printer.plug_ip,
"status": printer.status or "offline",
"active": printer.active if hasattr(printer, 'active') else True,
"created_at": printer.created_at.isoformat() if printer.created_at else datetime.now().isoformat()
}
db_session.close()
return render_template("admin_printer_settings.html", printer=printer_data)
except Exception as e:
db_session.close()
app_logger.error(f"Fehler beim Laden der Drucker-Einstellungen: {str(e)}")
flash("Fehler beim Laden der Drucker-Daten.", "error")
return redirect(url_for("admin_page"))
@app.route("/admin/guest-requests")
@login_required
@admin_required
def admin_guest_requests():
"""Admin-Seite für Gastanfragen Verwaltung"""
try:
app_logger.info(f"Admin-Gastanfragen Seite aufgerufen von User {current_user.id}")
return render_template("admin_guest_requests.html")
except Exception as e:
app_logger.error(f"Fehler beim Laden der Admin-Gastanfragen Seite: {str(e)}")
flash("Fehler beim Laden der Gastanfragen-Verwaltung.", "danger")
return redirect(url_for("admin"))
@app.route("/requests/overview")
@login_required
@admin_required
def admin_guest_requests_overview():
"""Admin-Oberfläche für die Verwaltung von Gastanfragen mit direkten Aktionen."""
try:
app_logger.info(f"Admin-Gastanträge Übersicht aufgerufen von User {current_user.id}")
return render_template("admin_guest_requests_overview.html")
except Exception as e:
app_logger.error(f"Fehler beim Laden der Admin-Gastanträge Übersicht: {str(e)}")
flash("Fehler beim Laden der Gastanträge-Übersicht.", "danger")
return redirect(url_for("admin"))
# ===== ADMIN API-ROUTEN FÜR BENUTZER UND DRUCKER =====
@app.route("/api/admin/users", methods=["POST"])
@login_required
def create_user_api():
"""Erstellt einen neuen Benutzer (nur für Admins)."""
if not current_user.is_admin:
return jsonify({"error": "Nur Administratoren können Benutzer erstellen"}), 403
try:
data = request.json
# Pflichtfelder prüfen
required_fields = ["username", "email", "password"]
for field in required_fields:
if field not in data or not data[field]:
return jsonify({"error": f"Feld '{field}' ist erforderlich"}), 400
db_session = get_db_session()
# Prüfen, ob bereits ein Benutzer mit diesem Benutzernamen oder E-Mail existiert
existing_user = db_session.query(User).filter(
(User.username == data["username"]) | (User.email == data["email"])
).first()
if existing_user:
db_session.close()
return jsonify({"error": "Ein Benutzer mit diesem Benutzernamen oder E-Mail existiert bereits"}), 400
# Neuen Benutzer erstellen
new_user = User(
username=data["username"],
email=data["email"],
first_name=data.get("first_name", ""),
last_name=data.get("last_name", ""),
is_admin=data.get("is_admin", False),
created_at=datetime.now()
)
# Passwort setzen
new_user.set_password(data["password"])
db_session.add(new_user)
db_session.commit()
user_data = {
"id": new_user.id,
"username": new_user.username,
"email": new_user.email,
"first_name": new_user.first_name,
"last_name": new_user.last_name,
"is_admin": new_user.is_admin,
"created_at": new_user.created_at.isoformat()
}
db_session.close()
user_logger.info(f"Neuer Benutzer '{new_user.username}' erstellt von Admin {current_user.id}")
return jsonify({"user": user_data}), 201
except Exception as e:
user_logger.error(f"Fehler beim Erstellen eines Benutzers: {str(e)}")
return jsonify({"error": "Interner Serverfehler"}), 500
@app.route("/api/admin/printers/<int:printer_id>/toggle", methods=["POST"])
@login_required
def toggle_printer_power(printer_id):
"""Schaltet einen Drucker ein oder aus (nur für Admins)."""
if not current_user.is_admin:
return jsonify({"error": "Nur Administratoren können Drucker steuern"}), 403
try:
data = request.json
power_on = data.get("power_on", True)
db_session = get_db_session()
printer = db_session.query(Printer).get(printer_id)
if not printer:
db_session.close()
return jsonify({"error": "Drucker nicht gefunden"}), 404
# Steckdose schalten
from utils.job_scheduler import toggle_plug
success = toggle_plug(printer_id, power_on)
if success:
# Status in der Datenbank aktualisieren
printer.status = "available" if power_on else "offline"
printer.active = power_on
db_session.commit()
action = "eingeschaltet" if power_on else "ausgeschaltet"
printers_logger.info(f"Drucker {printer.name} {action} von Admin {current_user.id}")
db_session.close()
return jsonify({
"success": True,
"message": f"Drucker erfolgreich {action}",
"status": printer.status
})
else:
db_session.close()
return jsonify({"error": "Fehler beim Schalten der Steckdose"}), 500
except Exception as e:
printers_logger.error(f"Fehler beim Schalten des Druckers {printer_id}: {str(e)}")
return jsonify({"error": "Interner Serverfehler"}), 500
# ===== ADMIN FORM ENDPOINTS =====
@app.route("/admin/users/create", methods=["POST"])
@login_required
def admin_create_user_form():
"""Erstellt einen neuen Benutzer über HTML-Form (nur für Admins)."""
if not current_user.is_admin:
flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error")
return redirect(url_for("index"))
try:
# Form-Daten lesen
email = request.form.get("email", "").strip()
name = request.form.get("name", "").strip()
password = request.form.get("password", "").strip()
role = request.form.get("role", "user").strip()
# Pflichtfelder prüfen
if not email or not password:
flash("E-Mail und Passwort sind erforderlich.", "error")
return redirect(url_for("admin_add_user_page"))
# E-Mail validieren
import re
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if not re.match(email_pattern, email):
flash("Ungültige E-Mail-Adresse.", "error")
return redirect(url_for("admin_add_user_page"))
db_session = get_db_session()
# Prüfen, ob bereits ein Benutzer mit dieser E-Mail existiert
existing_user = db_session.query(User).filter(User.email == email).first()
if existing_user:
db_session.close()
flash("Ein Benutzer mit dieser E-Mail existiert bereits.", "error")
return redirect(url_for("admin_add_user_page"))
# E-Mail als Username verwenden (falls kein separates Username-Feld)
username = email.split('@')[0]
counter = 1
original_username = username
while db_session.query(User).filter(User.username == username).first():
username = f"{original_username}{counter}"
counter += 1
# Neuen Benutzer erstellen
new_user = User(
username=username,
email=email,
first_name=name.split(' ')[0] if name else "",
last_name=" ".join(name.split(' ')[1:]) if name and ' ' in name else "",
is_admin=(role == "admin"),
created_at=datetime.now()
)
# Passwort setzen
new_user.set_password(password)
db_session.add(new_user)
db_session.commit()
db_session.close()
user_logger.info(f"Neuer Benutzer '{new_user.username}' erstellt von Admin {current_user.id}")
flash(f"Benutzer '{new_user.email}' erfolgreich erstellt.", "success")
return redirect(url_for("admin_page", tab="users"))
except Exception as e:
user_logger.error(f"Fehler beim Erstellen eines Benutzers über Form: {str(e)}")
flash("Fehler beim Erstellen des Benutzers.", "error")
return redirect(url_for("admin_add_user_page"))
@app.route("/admin/printers/create", methods=["POST"])
@login_required
def admin_create_printer_form():
"""Erstellt einen neuen Drucker über HTML-Form (nur für Admins)."""
if not current_user.is_admin:
flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error")
return redirect(url_for("index"))
try:
# Form-Daten lesen
name = request.form.get("name", "").strip()
ip_address = request.form.get("ip_address", "").strip()
model = request.form.get("model", "").strip()
location = request.form.get("location", "").strip()
description = request.form.get("description", "").strip()
status = request.form.get("status", "available").strip()
# Pflichtfelder prüfen
if not name or not ip_address:
flash("Name und IP-Adresse sind erforderlich.", "error")
return redirect(url_for("admin_add_printer_page"))
# IP-Adresse validieren
import re
ip_pattern = r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
if not re.match(ip_pattern, ip_address):
flash("Ungültige IP-Adresse.", "error")
return redirect(url_for("admin_add_printer_page"))
db_session = get_db_session()
# Prüfen, ob bereits ein Drucker mit diesem Namen existiert
existing_printer = db_session.query(Printer).filter(Printer.name == name).first()
if existing_printer:
db_session.close()
flash("Ein Drucker mit diesem Namen existiert bereits.", "error")
return redirect(url_for("admin_add_printer_page"))
# Neuen Drucker erstellen
new_printer = Printer(
name=name,
model=model,
location=location,
description=description,
mac_address="", # Wird später ausgefüllt
plug_ip=ip_address,
status=status,
created_at=datetime.now()
)
db_session.add(new_printer)
db_session.commit()
db_session.close()
printers_logger.info(f"Neuer Drucker '{new_printer.name}' erstellt von Admin {current_user.id}")
flash(f"Drucker '{new_printer.name}' erfolgreich erstellt.", "success")
return redirect(url_for("admin_page", tab="printers"))
except Exception as e:
printers_logger.error(f"Fehler beim Erstellen eines Druckers über Form: {str(e)}")
flash("Fehler beim Erstellen des Druckers.", "error")
return redirect(url_for("admin_add_printer_page"))
@app.route("/admin/users/<int:user_id>/edit", methods=["GET"])
@login_required
def admin_edit_user_page(user_id):
"""Zeigt die Benutzer-Bearbeitungsseite an."""
if not current_user.is_admin:
flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error")
return redirect(url_for("index"))
db_session = get_db_session()
try:
user = db_session.get(User, user_id)
if not user:
flash("Benutzer nicht gefunden.", "error")
return redirect(url_for("admin_page", tab="users"))
user_data = {
"id": user.id,
"username": user.username,
"email": user.email,
"name": user.name or "",
"is_admin": user.is_admin,
"active": user.active,
"created_at": user.created_at.isoformat() if user.created_at else datetime.now().isoformat()
}
db_session.close()
return render_template("admin_edit_user.html", user=user_data)
except Exception as e:
db_session.close()
app_logger.error(f"Fehler beim Laden der Benutzer-Daten: {str(e)}")
flash("Fehler beim Laden der Benutzer-Daten.", "error")
return redirect(url_for("admin_page", tab="users"))
@app.route("/admin/users/<int:user_id>/update", methods=["POST"])
@login_required
def admin_update_user_form(user_id):
"""Aktualisiert einen Benutzer über HTML-Form (nur für Admins)."""
if not current_user.is_admin:
flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error")
return redirect(url_for("index"))
try:
# Form-Daten lesen
email = request.form.get("email", "").strip()
name = request.form.get("name", "").strip()
password = request.form.get("password", "").strip()
role = request.form.get("role", "user").strip()
is_active = request.form.get("is_active", "true").strip() == "true"
# Pflichtfelder prüfen
if not email:
flash("E-Mail-Adresse ist erforderlich.", "error")
return redirect(url_for("admin_edit_user_page", user_id=user_id))
# E-Mail validieren
import re
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if not re.match(email_pattern, email):
flash("Ungültige E-Mail-Adresse.", "error")
return redirect(url_for("admin_edit_user_page", user_id=user_id))
db_session = get_db_session()
user = db_session.query(User).get(user_id)
if not user:
db_session.close()
flash("Benutzer nicht gefunden.", "error")
return redirect(url_for("admin_page", tab="users"))
# Prüfen, ob bereits ein anderer Benutzer mit dieser E-Mail existiert
existing_user = db_session.query(User).filter(
User.email == email,
User.id != user_id
).first()
if existing_user:
db_session.close()
flash("Ein Benutzer mit dieser E-Mail-Adresse existiert bereits.", "error")
return redirect(url_for("admin_edit_user_page", user_id=user_id))
# Benutzer aktualisieren
user.email = email
if name:
user.name = name
# Passwort nur ändern, wenn eines angegeben wurde
if password:
user.password_hash = generate_password_hash(password)
user.role = "admin" if role == "admin" else "user"
user.active = is_active
db_session.commit()
db_session.close()
auth_logger.info(f"Benutzer '{user.email}' (ID: {user_id}) aktualisiert von Admin {current_user.id}")
flash(f"Benutzer '{user.email}' erfolgreich aktualisiert.", "success")
return redirect(url_for("admin_page", tab="users"))
except Exception as e:
auth_logger.error(f"Fehler beim Aktualisieren eines Benutzers über Form: {str(e)}")
flash("Fehler beim Aktualisieren des Benutzers.", "error")
return redirect(url_for("admin_edit_user_page", user_id=user_id))
@app.route("/admin/printers/<int:printer_id>/update", methods=["POST"])
@login_required
def admin_update_printer_form(printer_id):
"""Aktualisiert einen Drucker über HTML-Form (nur für Admins)."""
if not current_user.is_admin:
flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error")
return redirect(url_for("index"))
try:
# Form-Daten lesen
name = request.form.get("name", "").strip()
ip_address = request.form.get("ip_address", "").strip()
model = request.form.get("model", "").strip()
location = request.form.get("location", "").strip()
description = request.form.get("description", "").strip()
status = request.form.get("status", "available").strip()
# Pflichtfelder prüfen
if not name or not ip_address:
flash("Name und IP-Adresse sind erforderlich.", "error")
return redirect(url_for("admin_printer_settings_page", printer_id=printer_id))
# IP-Adresse validieren
import re
ip_pattern = r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
if not re.match(ip_pattern, ip_address):
flash("Ungültige IP-Adresse.", "error")
return redirect(url_for("admin_printer_settings_page", printer_id=printer_id))
db_session = get_db_session()
printer = db_session.query(Printer).get(printer_id)
if not printer:
db_session.close()
flash("Drucker nicht gefunden.", "error")
return redirect(url_for("admin_page", tab="printers"))
# Prüfen, ob bereits ein anderer Drucker mit diesem Namen existiert
existing_printer = db_session.query(Printer).filter(
Printer.name == name,
Printer.id != printer_id
).first()
if existing_printer:
db_session.close()
flash("Ein Drucker mit diesem Namen existiert bereits.", "error")
return redirect(url_for("admin_printer_settings_page", printer_id=printer_id))
# Drucker aktualisieren
printer.name = name
printer.model = model
printer.location = location
printer.description = description
printer.plug_ip = ip_address
printer.status = status
db_session.commit()
db_session.close()
printers_logger.info(f"Drucker '{printer.name}' (ID: {printer_id}) aktualisiert von Admin {current_user.id}")
flash(f"Drucker '{printer.name}' erfolgreich aktualisiert.", "success")
return redirect(url_for("admin_manage_printer_page", printer_id=printer_id))
except Exception as e:
printers_logger.error(f"Fehler beim Aktualisieren eines Druckers über Form: {str(e)}")
flash("Fehler beim Aktualisieren des Druckers.", "error")
return redirect(url_for("admin_printer_settings_page", printer_id=printer_id))
# Neue API-Endpunkte für erweiterte Drucker-Status-Verwaltung hinzufügen
@app.route("/api/printers/online", methods=["GET"])
@login_required
def get_online_printers():
"""Gibt nur die online/verfügbaren Drucker zurück - optimiert für schnelle Anzeige."""
db_session = get_db_session()
printers_logger = get_logger("printers")
try:
# Session-Cache für Online-Drucker prüfen
cache_key = f"online_printers_{current_user.id}"
cached_data = session.get(cache_key)
cache_timestamp = session.get(f"{cache_key}_timestamp")
# Cache ist 30 Sekunden gültig
if cached_data and cache_timestamp:
cache_age = (datetime.now() - datetime.fromisoformat(cache_timestamp)).total_seconds()
if cache_age < 30:
printers_logger.info(f"Online-Drucker aus Session-Cache geladen (Alter: {cache_age:.1f}s)")
return jsonify({
"printers": cached_data,
"count": len(cached_data),
"cached": True,
"cache_age": cache_age
})
# Nur verfügbare/online Drucker aus Datenbank laden
printers = db_session.query(Printer).filter(
Printer.status.in_(["available", "online", "idle"]),
Printer.active == True
).all()
current_time = datetime.now()
online_printers = []
for printer in printers:
printer_data = {
"id": printer.id,
"name": printer.name,
"model": printer.model or 'Unbekanntes Modell',
"location": printer.location or 'Unbekannter Standort',
"mac_address": printer.mac_address,
"plug_ip": printer.plug_ip,
"status": printer.status,
"active": printer.active,
"ip_address": printer.plug_ip if printer.plug_ip else getattr(printer, 'ip_address', None),
"created_at": printer.created_at.isoformat() if printer.created_at else current_time.isoformat(),
"last_checked": printer.last_checked.isoformat() if hasattr(printer, 'last_checked') and printer.last_checked else None,
"is_online": True # Alle Drucker in dieser Liste sind online
}
online_printers.append(printer_data)
# In Session-Cache speichern
session[cache_key] = online_printers
session[f"{cache_key}_timestamp"] = current_time.isoformat()
session.permanent = True
db_session.close()
printers_logger.info(f"Online-Drucker geladen: {len(online_printers)} verfügbare Drucker")
return jsonify({
"printers": online_printers,
"count": len(online_printers),
"cached": False,
"message": f"{len(online_printers)} online Drucker gefunden"
})
except Exception as e:
db_session.rollback()
db_session.close()
printers_logger.error(f"Fehler beim Abrufen der Online-Drucker: {str(e)}")
return jsonify({
"error": f"Fehler beim Laden der Online-Drucker: {str(e)}",
"printers": []
}), 500
@app.route("/api/printers/status/live", methods=["GET"])
@login_required
@measure_execution_time(logger=printers_logger, task_name="API-Live-Drucker-Status")
def get_live_printer_status():
"""Gibt Live-Status aller Drucker zurück mit Session-Caching und Echtzeit-Updates."""
db_session = get_db_session()
printers_logger = get_logger("printers")
try:
# Session-Cache für Live-Status prüfen
cache_key = f"live_printer_status_{current_user.id}"
cached_data = session.get(cache_key)
cache_timestamp = session.get(f"{cache_key}_timestamp")
# Cache ist 15 Sekunden gültig für Live-Status
if cached_data and cache_timestamp:
cache_age = (datetime.now() - datetime.fromisoformat(cache_timestamp)).total_seconds()
if cache_age < 15:
printers_logger.info(f"Live-Status aus Session-Cache geladen (Alter: {cache_age:.1f}s)")
return jsonify({
"printers": cached_data,
"cached": True,
"cache_age": cache_age,
"next_update": 15 - cache_age
})
# Alle Drucker aus der Datenbank laden
printers = db_session.query(Printer).all()
if not printers:
return jsonify({
"printers": [],
"count": 0,
"message": "Keine Drucker in der Datenbank gefunden"
})
# Drucker-Daten für Status-Check vorbereiten
printer_data = []
for printer in printers:
printer_data.append({
'id': printer.id,
'name': printer.name,
'ip_address': printer.plug_ip if printer.plug_ip else getattr(printer, 'ip_address', None)
})
# Paralleler Status-Check mit kürzerem Timeout für Live-Updates
try:
status_results = check_multiple_printers_status(printer_data, timeout=3)
except Exception as e:
printers_logger.warning(f"Status-Check fehlgeschlagen, verwende letzte bekannte Status: {str(e)}")
# Fallback: verwende letzte bekannte Status
status_results = {p['id']: (p.get('last_status', 'offline'), False) for p in printer_data}
# Live-Status-Daten zusammenstellen
live_status_data = []
current_time = datetime.now()
online_count = 0
for printer in printers:
if printer.id in status_results:
status, active = status_results[printer.id]
frontend_status = "available" if status == "online" else "offline"
if frontend_status == "available":
online_count += 1
else:
frontend_status = printer.status or "offline"
active = printer.active if hasattr(printer, 'active') else False
# Status in Datenbank aktualisieren (asynchron)
printer.status = frontend_status
printer.active = active
if hasattr(printer, 'last_checked'):
printer.last_checked = current_time
live_status_data.append({
"id": printer.id,
"name": printer.name,
"model": printer.model or 'Unbekanntes Modell',
"location": printer.location or 'Unbekannter Standort',
"mac_address": printer.mac_address,
"plug_ip": printer.plug_ip,
"status": frontend_status,
"active": active,
"ip_address": printer.plug_ip if printer.plug_ip else getattr(printer, 'ip_address', None),
"created_at": printer.created_at.isoformat() if printer.created_at else current_time.isoformat(),
"last_checked": current_time.isoformat(),
"is_online": frontend_status == "available",
"status_changed": True # Für Frontend-Animationen
})
# Änderungen in Datenbank speichern
try:
db_session.commit()
except Exception as e:
printers_logger.warning(f"Fehler beim Speichern der Live-Status-Updates: {str(e)}")
# In Session-Cache speichern
session[cache_key] = live_status_data
session[f"{cache_key}_timestamp"] = current_time.isoformat()
session.permanent = True
# Online-Drucker-Cache invalidieren
online_cache_key = f"online_printers_{current_user.id}"
if online_cache_key in session:
del session[online_cache_key]
del session[f"{online_cache_key}_timestamp"]
db_session.close()
printers_logger.info(f"Live-Status aktualisiert: {online_count} von {len(live_status_data)} Drucker online")
return jsonify({
"printers": live_status_data,
"count": len(live_status_data),
"online_count": online_count,
"offline_count": len(live_status_data) - online_count,
"cached": False,
"timestamp": current_time.isoformat(),
"next_update": 15
})
except Exception as e:
db_session.rollback()
db_session.close()
printers_logger.error(f"Fehler beim Live-Status-Check: {str(e)}")
return jsonify({
"error": f"Fehler beim Live-Status-Check: {str(e)}",
"printers": []
}), 500
@app.route("/api/printers/status/summary", methods=["GET"])
@login_required
def get_printer_status_summary():
"""Gibt eine Zusammenfassung des Drucker-Status zurück - sehr schnell."""
db_session = get_db_session()
try:
# Session-Cache für Status-Zusammenfassung
cache_key = f"printer_summary_{current_user.id}"
cached_data = session.get(cache_key)
cache_timestamp = session.get(f"{cache_key}_timestamp")
# Cache ist 60 Sekunden gültig
if cached_data and cache_timestamp:
cache_age = (datetime.now() - datetime.fromisoformat(cache_timestamp)).total_seconds()
if cache_age < 60:
return jsonify({
**cached_data,
"cached": True,
"cache_age": cache_age
})
# Status-Zusammenfassung aus Datenbank
total_printers = db_session.query(Printer).count()
online_printers = db_session.query(Printer).filter(
Printer.status.in_(["available", "online", "idle"]),
Printer.active == True
).count()
offline_printers = total_printers - online_printers
# Letzte Aktualisierung ermitteln
last_checked = db_session.query(func.max(Printer.last_checked)).scalar()
summary_data = {
"total": total_printers,
"online": online_printers,
"offline": offline_printers,
"percentage_online": round((online_printers / total_printers * 100) if total_printers > 0 else 0, 1),
"last_checked": last_checked.isoformat() if last_checked else None,
"timestamp": datetime.now().isoformat()
}
# In Session-Cache speichern
session[cache_key] = summary_data
session[f"{cache_key}_timestamp"] = datetime.now().isoformat()
session.permanent = True
db_session.close()
return jsonify({
**summary_data,
"cached": False
})
except Exception as e:
db_session.close()
return jsonify({
"error": f"Fehler beim Laden der Status-Zusammenfassung: {str(e)}",
"total": 0,
"online": 0,
"offline": 0
}), 500
# Session-Cache-Management
@app.route("/api/printers/cache/clear", methods=["POST"])
@login_required
def clear_printer_cache():
"""Löscht den Drucker-Cache für den aktuellen Benutzer."""
try:
cache_keys = [
f"online_printers_{current_user.id}",
f"live_printer_status_{current_user.id}",
f"printer_summary_{current_user.id}"
]
cleared_count = 0
for key in cache_keys:
if key in session:
del session[key]
cleared_count += 1
timestamp_key = f"{key}_timestamp"
if timestamp_key in session:
del session[timestamp_key]
return jsonify({
"message": f"Cache erfolgreich geleert ({cleared_count} Einträge)",
"cleared_keys": cleared_count
})
except Exception as e:
return jsonify({
"error": f"Fehler beim Löschen des Cache: {str(e)}"
}), 500
# ===== FEHLENDE ADMIN-API-ENDPUNKTE =====
@app.route('/api/admin/cache/clear', methods=['POST'])
@admin_required
def clear_admin_cache():
"""Leert den System-Cache"""
try:
# Cache-Verzeichnisse leeren
import shutil
import os
cache_dirs = [
os.path.join(os.path.dirname(__file__), 'static', 'cache'),
os.path.join(os.path.dirname(__file__), '__pycache__'),
]
cleared_items = 0
for cache_dir in cache_dirs:
if os.path.exists(cache_dir):
for item in os.listdir(cache_dir):
item_path = os.path.join(cache_dir, item)
try:
if os.path.isfile(item_path):
os.unlink(item_path)
cleared_items += 1
elif os.path.isdir(item_path):
shutil.rmtree(item_path)
cleared_items += 1
except Exception as e:
app_logger.warning(f"Konnte Cache-Element nicht löschen: {item_path} - {str(e)}")
# Modell-Cache leeren
try:
from models import clear_cache
clear_cache()
except (ImportError, AttributeError):
app_logger.warning("clear_cache Funktion nicht verfügbar")
app_logger.info(f"System-Cache geleert: {cleared_items} Elemente entfernt")
return jsonify({
"success": True,
"message": f"Cache erfolgreich geleert ({cleared_items} Elemente)",
"cleared_items": cleared_items
})
except Exception as e:
app_logger.error(f"Fehler beim Leeren des Cache: {str(e)}")
return jsonify({
"success": False,
"message": f"Fehler beim Leeren des Cache: {str(e)}"
}), 500
@app.route('/api/admin/system/restart', methods=['POST'])
@admin_required
def restart_admin_system():
"""Startet das System neu (nur für Entwicklung)"""
try:
import os
import signal
app_logger.warning("System-Neustart durch Admin angefordert")
# In Produktionsumgebung sollte dies anders gehandhabt werden
if os.environ.get('FLASK_ENV') == 'development':
# Graceful shutdown für Development
def shutdown_server():
func = request.environ.get('werkzeug.server.shutdown')
if func is None:
raise RuntimeError('Not running with the Werkzeug Server')
func()
shutdown_server()
return jsonify({
"success": True,
"message": "System wird neugestartet..."
})
else:
# Für Produktion - Signal an Parent Process
os.kill(os.getpid(), signal.SIGTERM)
return jsonify({
"success": True,
"message": "Neustart-Signal gesendet"
})
except Exception as e:
app_logger.error(f"Fehler beim System-Neustart: {str(e)}")
return jsonify({
"success": False,
"message": f"Fehler beim Neustart: {str(e)}"
}), 500
@app.route('/api/admin/printers/update-all', methods=['POST'])
@admin_required
def update_all_printers():
"""Aktualisiert den Status aller Drucker"""
try:
db_session = get_db_session()
printers = db_session.query(Printer).all()
updated_printers = []
for printer in printers:
if printer.plug_ip:
try:
status, active = check_printer_status(printer.plug_ip)
old_status = printer.status
printer.update_status(status, active)
updated_printers.append({
"id": printer.id,
"name": printer.name,
"old_status": old_status,
"new_status": status,
"active": active
})
except Exception as e:
printers_logger.warning(f"Fehler beim Aktualisieren von Drucker {printer.name}: {str(e)}")
db_session.commit()
db_session.close()
app_logger.info(f"Status von {len(updated_printers)} Druckern aktualisiert")
return jsonify({
"success": True,
"message": f"Status von {len(updated_printers)} Druckern aktualisiert",
"updated_printers": updated_printers
})
except Exception as e:
app_logger.error(f"Fehler beim Aktualisieren aller Drucker: {str(e)}")
return jsonify({
"success": False,
"message": f"Fehler beim Aktualisieren: {str(e)}"
}), 500
@app.route('/api/admin/settings', methods=['GET'])
@admin_required
def get_admin_settings():
"""Holt die aktuellen Admin-Einstellungen"""
try:
from config.settings import (
FLASK_HOST, FLASK_PORT, FLASK_DEBUG, SESSION_LIFETIME,
SCHEDULER_INTERVAL, SCHEDULER_ENABLED, SSL_ENABLED
)
settings = {
"server": {
"host": FLASK_HOST,
"port": FLASK_PORT,
"debug": FLASK_DEBUG,
"ssl_enabled": SSL_ENABLED
},
"session": {
"lifetime_minutes": SESSION_LIFETIME.total_seconds() / 60
},
"scheduler": {
"interval_seconds": SCHEDULER_INTERVAL,
"enabled": SCHEDULER_ENABLED
}
}
return jsonify({
"success": True,
"settings": settings
})
except Exception as e:
app_logger.error(f"Fehler beim Laden der Admin-Einstellungen: {str(e)}")
return jsonify({
"success": False,
"message": f"Fehler beim Laden der Einstellungen: {str(e)}"
}), 500
@app.route('/api/admin/settings', methods=['POST'])
@admin_required
def update_admin_settings():
"""Aktualisiert die Admin-Einstellungen"""
try:
data = request.get_json()
if not data:
return jsonify({
"success": False,
"message": "Keine Daten empfangen"
}), 400
# Hier würden normalerweise die Einstellungen in einer Konfigurationsdatei gespeichert
# Für diese Demo loggen wir nur die Änderungen
app_logger.info(f"Admin-Einstellungen aktualisiert: {data}")
return jsonify({
"success": True,
"message": "Einstellungen erfolgreich aktualisiert"
})
except Exception as e:
app_logger.error(f"Fehler beim Aktualisieren der Admin-Einstellungen: {str(e)}")
return jsonify({
"success": False,
"message": f"Fehler beim Aktualisieren: {str(e)}"
}), 500
@app.route('/api/admin/logs/export', methods=['GET'])
@admin_required
def export_admin_logs():
"""Exportiert System-Logs"""
try:
import os
import zipfile
import tempfile
from datetime import datetime
# Temporäre ZIP-Datei erstellen
temp_dir = tempfile.mkdtemp()
zip_filename = f"myp_logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
zip_path = os.path.join(temp_dir, zip_filename)
log_dir = os.path.join(os.path.dirname(__file__), 'logs')
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
for root, dirs, files in os.walk(log_dir):
for file in files:
if file.endswith('.log'):
file_path = os.path.join(root, file)
arcname = os.path.relpath(file_path, log_dir)
zipf.write(file_path, arcname)
app_logger.info("System-Logs exportiert")
return send_file(zip_path, as_attachment=True, download_name=zip_filename)
except Exception as e:
app_logger.error(f"Fehler beim Exportieren der Logs: {str(e)}")
return jsonify({
"success": False,
"message": f"Fehler beim Exportieren: {str(e)}"
}), 500
@app.route('/api/logs', methods=['GET'])
@login_required
def get_system_logs():
"""API-Endpunkt zum Laden der System-Logs für das Dashboard."""
if not current_user.is_admin:
return jsonify({"success": False, "error": "Berechtigung verweigert"}), 403
try:
import os
from datetime import datetime
log_level = request.args.get('log_level', 'all')
log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logs')
# Logeinträge sammeln
app_logs = []
for category in ['app', 'auth', 'jobs', 'printers', 'scheduler', 'errors']:
log_file = os.path.join(log_dir, category, f'{category}.log')
if os.path.exists(log_file):
try:
with open(log_file, 'r', encoding='utf-8') as f:
lines = f.readlines()
# Nur die letzten 100 Zeilen pro Datei
for line in lines[-100:]:
line = line.strip()
if not line:
continue
# Log-Level-Filter anwenden
if log_level != 'all':
if log_level.upper() not in line:
continue
# Log-Eintrag parsen
parts = line.split(' - ')
if len(parts) >= 3:
timestamp = parts[0]
level = parts[1]
message = ' - '.join(parts[2:])
else:
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
level = 'INFO'
message = line
app_logs.append({
'timestamp': timestamp,
'level': level,
'category': category,
'module': category,
'message': message,
'source': category
})
except Exception as file_error:
app_logger.warning(f"Fehler beim Lesen der Log-Datei {log_file}: {str(file_error)}")
continue
# Nach Zeitstempel sortieren (neueste zuerst)
try:
logs = sorted(app_logs, key=lambda x: x['timestamp'] if x['timestamp'] else '', reverse=True)[:100]
except:
# Falls Sortierung fehlschlägt, einfach die letzten 100 nehmen
logs = app_logs[-100:]
app_logger.info(f"Logs erfolgreich geladen: {len(logs)} Einträge")
return jsonify({
"success": True,
"logs": logs,
"count": len(logs),
"message": f"{len(logs)} Log-Einträge geladen"
})
except Exception as e:
app_logger.error(f"Fehler beim Laden der Logs: {str(e)}")
return jsonify({
"success": False,
"error": "Fehler beim Laden der Logs",
"message": str(e),
"logs": []
}), 500
# ===== ENDE FEHLENDE ADMIN-API-ENDPUNKTE =====
# ===== BENACHRICHTIGUNGS-API-ENDPUNKTE =====
@app.route('/api/notifications', methods=['GET'])
@login_required
def get_notifications():
"""Holt alle Benachrichtigungen für den aktuellen Benutzer"""
try:
db_session = get_db_session()
# Sicherstellen, dass current_user.id als Integer behandelt wird
user_id = int(current_user.id)
# Benachrichtigungen für den aktuellen Benutzer laden
notifications = db_session.query(Notification).filter(
Notification.user_id == user_id
).order_by(Notification.created_at.desc()).limit(50).all()
notifications_data = [notification.to_dict() for notification in notifications]
db_session.close()
return jsonify({
"success": True,
"notifications": notifications_data
})
except Exception as e:
app_logger.error(f"Fehler beim Laden der Benachrichtigungen: {str(e)}")
return jsonify({
"success": False,
"message": f"Fehler beim Laden der Benachrichtigungen: {str(e)}"
}), 500
@app.route('/api/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()
# Sicherstellen, dass current_user.id als Integer behandelt wird
user_id = int(current_user.id)
notification = db_session.query(Notification).filter(
Notification.id == notification_id,
Notification.user_id == user_id
).first()
if not notification:
db_session.close()
return jsonify({
"success": False,
"message": "Benachrichtigung nicht gefunden"
}), 404
notification.read = True
db_session.commit()
db_session.close()
return jsonify({
"success": True,
"message": "Benachrichtigung als gelesen markiert"
})
except Exception as e:
app_logger.error(f"Fehler beim Markieren der Benachrichtigung: {str(e)}")
return jsonify({
"success": False,
"message": f"Fehler beim Markieren: {str(e)}"
}), 500
@app.route('/api/notifications/mark-all-read', methods=['POST'])
@login_required
def mark_all_notifications_read():
"""Markiert alle Benachrichtigungen als gelesen"""
try:
db_session = get_db_session()
# Sicherstellen, dass current_user.id als Integer behandelt wird
user_id = int(current_user.id)
# Alle ungelesenen Benachrichtigungen des Benutzers finden und als gelesen markieren
updated_count = db_session.query(Notification).filter(
Notification.user_id == user_id,
Notification.read == False
).update({"read": True})
db_session.commit()
db_session.close()
return jsonify({
"success": True,
"message": f"{updated_count} Benachrichtigungen als gelesen markiert"
})
except Exception as e:
app_logger.error(f"Fehler beim Markieren aller Benachrichtigungen: {str(e)}")
return jsonify({
"success": False,
"message": f"Fehler beim Markieren: {str(e)}"
}), 500
# ===== ENDE BENACHRICHTIGUNGS-API-ENDPUNKTE =====
# ===== QUEUE-MANAGER-API-ENDPUNKTE =====
@app.route('/api/queue/status', methods=['GET'])
@login_required
def get_queue_status():
"""Gibt den aktuellen Status der Drucker-Warteschlangen zurück."""
try:
queue_manager = get_queue_manager()
status = queue_manager.get_queue_status()
return jsonify({
"success": True,
"queue_status": status
})
except Exception as e:
app_logger.error(f"Fehler beim Abrufen des Queue-Status: {str(e)}")
return jsonify({
"success": False,
"message": f"Fehler beim Abrufen des Queue-Status: {str(e)}"
}), 500
@app.route('/api/queue/check-now', methods=['POST'])
@login_required
def trigger_queue_check():
"""Triggert eine sofortige Überprüfung der Warteschlangen."""
try:
# Bestehende check_waiting_jobs API verwenden
return check_waiting_jobs()
except Exception as e:
app_logger.error(f"Fehler beim manuellen Queue-Check: {str(e)}")
return jsonify({
"success": False,
"message": f"Fehler beim manuellen Queue-Check: {str(e)}"
}), 500
# ===== ENDE QUEUE-MANAGER-API-ENDPUNKTE =====
# ===== NEUE ADMIN API-ROUTEN FÜR BUTTON-FUNKTIONALITÄTEN =====
@app.route('/api/admin/maintenance/activate', methods=['POST'])
@admin_required
def activate_maintenance_mode():
"""Aktiviert den Wartungsmodus"""
try:
# Hier würde die Wartungsmodus-Logik implementiert werden
# Zum Beispiel: Setze einen globalen Flag, blockiere neue Jobs, etc.
# Für Demo-Zwecke simulieren wir die Aktivierung
app_logger.info("Wartungsmodus aktiviert durch Admin")
return jsonify({
"success": True,
"message": "Wartungsmodus wurde aktiviert"
})
except Exception as e:
app_logger.error(f"Fehler beim Aktivieren des Wartungsmodus: {str(e)}")
return jsonify({
"success": False,
"error": "Fehler beim Aktivieren des Wartungsmodus"
}), 500
@app.route('/api/admin/maintenance/deactivate', methods=['POST'])
@admin_required
def deactivate_maintenance_mode():
"""Deaktiviert den Wartungsmodus"""
try:
# Hier würde die Wartungsmodus-Deaktivierung implementiert werden
app_logger.info("Wartungsmodus deaktiviert durch Admin")
return jsonify({
"success": True,
"message": "Wartungsmodus wurde deaktiviert"
})
except Exception as e:
app_logger.error(f"Fehler beim Deaktivieren des Wartungsmodus: {str(e)}")
return jsonify({
"success": False,
"error": "Fehler beim Deaktivieren des Wartungsmodus"
}), 500
@app.route('/api/admin/stats/live', methods=['GET'])
@admin_required
def get_live_admin_stats():
"""Liefert Live-Statistiken für das Admin-Dashboard"""
try:
db_session = get_db_session()
# Benutzer-Statistiken
total_users = db_session.query(func.count(User.id)).scalar() or 0
# Drucker-Statistiken
total_printers = db_session.query(func.count(Printer.id)).scalar() or 0
online_printers = db_session.query(func.count(Printer.id)).filter(
Printer.status.in_(['online', 'idle'])
).scalar() or 0
# Job-Statistiken
active_jobs = db_session.query(func.count(Job.id)).filter(
Job.status == 'running'
).scalar() or 0
queued_jobs = db_session.query(func.count(Job.id)).filter(
Job.status == 'queued'
).scalar() or 0
# Erfolgsrate berechnen
total_jobs = db_session.query(func.count(Job.id)).scalar() or 1
completed_jobs = db_session.query(func.count(Job.id)).filter(
Job.status == 'completed'
).scalar() or 0
success_rate = round((completed_jobs / total_jobs) * 100, 1) if total_jobs > 0 else 0
db_session.close()
return jsonify({
"success": True,
"stats": {
"total_users": total_users,
"total_printers": total_printers,
"online_printers": online_printers,
"active_jobs": active_jobs,
"queued_jobs": queued_jobs,
"success_rate": success_rate
}
})
except Exception as e:
app_logger.error(f"Fehler beim Laden der Live-Admin-Statistiken: {str(e)}")
return jsonify({
"success": False,
"error": "Fehler beim Laden der Statistiken"
}), 500
@app.route('/api/admin/system/status', methods=['GET'])
@admin_required
def get_admin_system_status():
"""Liefert detaillierte System-Status-Informationen"""
try:
import psutil
import os
from datetime import datetime, timedelta
# CPU-Nutzung
cpu_usage = round(psutil.cpu_percent(interval=1), 1)
# RAM-Nutzung
memory = psutil.virtual_memory()
memory_usage = round(memory.percent, 1)
# Festplatten-Nutzung
disk = psutil.disk_usage('/')
disk_usage = round((disk.used / disk.total) * 100, 1)
# System-Uptime
boot_time = datetime.fromtimestamp(psutil.boot_time())
uptime = datetime.now() - boot_time
uptime_str = f"{uptime.days}d {uptime.seconds//3600}h {(uptime.seconds//60)%60}m"
# Datenbankverbindung testen
db_session = get_db_session()
db_status = "Verbunden"
try:
db_session.execute("SELECT 1")
db_session.close()
except:
db_status = "Fehler"
db_session.close()
return jsonify({
"success": True,
"status": {
"cpu_usage": cpu_usage,
"memory_usage": memory_usage,
"disk_usage": disk_usage,
"uptime": uptime_str,
"database_status": db_status,
"timestamp": datetime.now().isoformat()
}
})
except ImportError:
# Falls psutil nicht verfügbar ist, Dummy-Daten zurückgeben
return jsonify({
"success": True,
"status": {
"cpu_usage": 15.2,
"memory_usage": 42.8,
"disk_usage": 67.3,
"uptime": "2d 14h 32m",
"database_status": "Verbunden",
"timestamp": datetime.now().isoformat()
}
})
except Exception as e:
app_logger.error(f"Fehler beim Laden des System-Status: {str(e)}")
return jsonify({
"success": False,
"error": "Fehler beim Laden des System-Status"
}), 500
@app.route('/api/dashboard/stats', methods=['GET'])
@login_required
def get_dashboard_stats():
"""Liefert Dashboard-Statistiken für Hintergrund-Updates"""
try:
db_session = get_db_session()
# Aktive Jobs zählen
active_jobs_count = db_session.query(func.count(Job.id)).filter(
Job.status == 'running'
).scalar() or 0
# Verfügbare Drucker zählen
available_printers_count = db_session.query(func.count(Printer.id)).filter(
Printer.status.in_(['online', 'idle'])
).scalar() or 0
# Gesamte Jobs zählen
total_jobs_count = db_session.query(func.count(Job.id)).scalar() or 0
# Erfolgsrate berechnen
completed_jobs = db_session.query(func.count(Job.id)).filter(
Job.status == 'completed'
).scalar() or 0
success_rate = round((completed_jobs / total_jobs_count) * 100, 1) if total_jobs_count > 0 else 100.0
db_session.close()
return jsonify({
"success": True,
"active_jobs_count": active_jobs_count,
"available_printers_count": available_printers_count,
"total_jobs_count": total_jobs_count,
"success_rate": success_rate
})
except Exception as e:
app_logger.error(f"Fehler beim Laden der Dashboard-Statistiken: {str(e)}")
return jsonify({
"success": False,
"error": "Fehler beim Laden der Statistiken"
}), 500
@app.route('/api/dashboard/active-jobs', methods=['GET'])
@login_required
def get_dashboard_active_jobs():
"""Liefert aktive Jobs für Dashboard-Updates"""
try:
db_session = get_db_session()
active_jobs = db_session.query(Job).filter(
Job.status.in_(['running', 'paused'])
).limit(5).all()
jobs_data = []
for job in active_jobs:
jobs_data.append({
"id": job.id,
"name": job.name,
"status": job.status,
"progress": getattr(job, 'progress', 0),
"printer": job.printer.name if job.printer else 'Unbekannt',
"start_time": job.created_at.strftime('%H:%M') if job.created_at else '--:--'
})
db_session.close()
return jsonify({
"success": True,
"jobs": jobs_data
})
except Exception as e:
app_logger.error(f"Fehler beim Laden der aktiven Jobs: {str(e)}")
return jsonify({
"success": False,
"error": "Fehler beim Laden der aktiven Jobs"
}), 500
@app.route('/api/dashboard/printers', methods=['GET'])
@login_required
def get_dashboard_printers():
"""Liefert Drucker-Status für Dashboard-Updates"""
try:
db_session = get_db_session()
printers = db_session.query(Printer).limit(5).all()
printers_data = []
for printer in printers:
printers_data.append({
"id": printer.id,
"name": printer.name,
"status": printer.status,
"location": printer.location,
"model": printer.model
})
db_session.close()
return jsonify({
"success": True,
"printers": printers_data
})
except Exception as e:
app_logger.error(f"Fehler beim Laden der Drucker: {str(e)}")
return jsonify({
"success": False,
"error": "Fehler beim Laden der Drucker"
}), 500
@app.route('/api/dashboard/activities', methods=['GET'])
@login_required
def get_dashboard_activities():
"""Liefert die neuesten Aktivitäten für das Dashboard"""
try:
db_session = get_db_session()
# Neueste Jobs abrufen
activities = []
recent_jobs = db_session.query(Job).order_by(Job.created_at.desc()).limit(10).all()
for job in recent_jobs:
activities.append({
'description': f"Job '{job.name}' wurde {job.status}",
'time': job.created_at.strftime('%H:%M'),
'type': 'job',
'status': job.status
})
db_session.close()
return jsonify({
'success': True,
'activities': activities
})
except Exception as e:
app_logger.error(f"Fehler beim Abrufen der Dashboard-Aktivitäten: {str(e)}")
return jsonify({
'success': False,
'error': 'Fehler beim Laden der Aktivitäten'
}), 500
@app.route('/admin/settings', methods=['GET'])
@login_required
@admin_required
def admin_settings():
"""Admin-Einstellungen Seite"""
try:
return render_template('admin_settings.html')
except Exception as e:
app_logger.error(f"Fehler beim Laden der Admin-Einstellungen: {str(e)}")
flash("Fehler beim Laden der Einstellungen", "error")
return redirect(url_for('admin_page'))
@app.route('/analytics', methods=['GET'])
@login_required
def analytics_page():
"""Analytics Seite"""
try:
return render_template('analytics.html')
except Exception as e:
app_logger.error(f"Fehler beim Laden der Analytics-Seite: {str(e)}")
flash("Fehler beim Laden der Analytics", "error")
return redirect(url_for('dashboard'))
@app.route('/api/optimization/auto-optimize', methods=['POST'])
@login_required
def auto_optimize_jobs():
"""Automatische Optimierung der Druckaufträge durchführen"""
try:
data = request.get_json()
settings = data.get('settings', {})
enabled = data.get('enabled', False)
db_session = get_db_session()
# Aktuelle Jobs in der Warteschlange abrufen
pending_jobs = db_session.query(Job).filter(
Job.status.in_(['queued', 'pending'])
).all()
if not pending_jobs:
db_session.close()
return jsonify({
'success': True,
'message': 'Keine Jobs zur Optimierung verfügbar',
'optimized_jobs': 0
})
# Verfügbare Drucker abrufen
available_printers = db_session.query(Printer).filter(Printer.active == True).all()
if not available_printers:
db_session.close()
return jsonify({
'success': False,
'error': 'Keine verfügbaren Drucker für Optimierung'
})
# Optimierungs-Algorithmus anwenden
algorithm = settings.get('algorithm', 'round_robin')
optimized_count = 0
if algorithm == 'round_robin':
optimized_count = apply_round_robin_optimization(pending_jobs, available_printers, db_session)
elif algorithm == 'load_balance':
optimized_count = apply_load_balance_optimization(pending_jobs, available_printers, db_session)
elif algorithm == 'priority_based':
optimized_count = apply_priority_optimization(pending_jobs, available_printers, db_session)
db_session.commit()
jobs_logger.info(f"Auto-Optimierung durchgeführt: {optimized_count} Jobs optimiert mit Algorithmus {algorithm}")
# System-Log erstellen
log_entry = SystemLog(
level='INFO',
component='optimization',
message=f'Auto-Optimierung durchgeführt: {optimized_count} Jobs optimiert',
user_id=current_user.id if current_user.is_authenticated else None,
details=json.dumps({
'algorithm': algorithm,
'optimized_jobs': optimized_count,
'settings': settings
})
)
db_session.add(log_entry)
db_session.commit()
db_session.close()
return jsonify({
'success': True,
'optimized_jobs': optimized_count,
'algorithm': algorithm,
'message': f'Optimierung erfolgreich: {optimized_count} Jobs wurden optimiert'
})
except Exception as e:
app_logger.error(f"Fehler bei der Auto-Optimierung: {str(e)}")
return jsonify({
'success': False,
'error': f'Optimierung fehlgeschlagen: {str(e)}'
}), 500
@app.route('/api/jobs/batch-operation', methods=['POST'])
@login_required
def perform_batch_operation():
"""Batch-Operationen auf mehrere Jobs anwenden"""
try:
data = request.get_json()
job_ids = data.get('job_ids', [])
operation = data.get('operation', '')
if not job_ids:
return jsonify({
'success': False,
'error': 'Keine Job-IDs für Batch-Operation angegeben'
}), 400
if not operation:
return jsonify({
'success': False,
'error': 'Keine Operation für Batch-Verarbeitung angegeben'
}), 400
db_session = get_db_session()
# Jobs abrufen (nur eigene oder Admin-Rechte prüfen)
if current_user.is_admin:
jobs = db_session.query(Job).filter(Job.id.in_(job_ids)).all()
else:
jobs = db_session.query(Job).filter(
Job.id.in_(job_ids),
Job.user_id == int(current_user.id)
).all()
if not jobs:
db_session.close()
return jsonify({
'success': False,
'error': 'Keine berechtigten Jobs für Batch-Operation gefunden'
}), 403
processed_count = 0
error_count = 0
# Batch-Operation durchführen
for job in jobs:
try:
if operation == 'start':
if job.status in ['queued', 'pending']:
job.status = 'running'
job.start_time = datetime.now()
processed_count += 1
elif operation == 'pause':
if job.status == 'running':
job.status = 'paused'
processed_count += 1
elif operation == 'cancel':
if job.status in ['queued', 'pending', 'running', 'paused']:
job.status = 'cancelled'
job.end_time = datetime.now()
processed_count += 1
elif operation == 'delete':
if job.status in ['completed', 'cancelled', 'failed']:
db_session.delete(job)
processed_count += 1
elif operation == 'restart':
if job.status in ['failed', 'cancelled']:
job.status = 'queued'
job.start_time = None
job.end_time = None
processed_count += 1
elif operation == 'priority_high':
job.priority = 'high'
processed_count += 1
elif operation == 'priority_normal':
job.priority = 'normal'
processed_count += 1
else:
jobs_logger.warning(f"Unbekannte Batch-Operation: {operation}")
error_count += 1
except Exception as job_error:
jobs_logger.error(f"Fehler bei Job {job.id} in Batch-Operation {operation}: {str(job_error)}")
error_count += 1
db_session.commit()
# System-Log erstellen
log_entry = SystemLog(
level='INFO',
component='batch_operations',
message=f'Batch-Operation "{operation}" durchgeführt: {processed_count} Jobs verarbeitet',
user_id=current_user.id,
details=json.dumps({
'operation': operation,
'processed_jobs': processed_count,
'error_count': error_count,
'job_ids': job_ids
})
)
db_session.add(log_entry)
db_session.commit()
db_session.close()
jobs_logger.info(f"Batch-Operation {operation} durchgeführt: {processed_count} Jobs verarbeitet, {error_count} Fehler")
return jsonify({
'success': True,
'processed_jobs': processed_count,
'error_count': error_count,
'operation': operation,
'message': f'Batch-Operation erfolgreich: {processed_count} Jobs verarbeitet'
})
except Exception as e:
app_logger.error(f"Fehler bei Batch-Operation: {str(e)}")
return jsonify({
'success': False,
'error': f'Batch-Operation fehlgeschlagen: {str(e)}'
}), 500
@app.route('/api/optimization/settings', methods=['GET', 'POST'])
@login_required
def optimization_settings():
"""Optimierungs-Einstellungen abrufen und speichern"""
db_session = get_db_session()
if request.method == 'GET':
try:
# Standard-Einstellungen oder benutzerdefinierte laden
default_settings = {
'algorithm': 'round_robin',
'consider_distance': True,
'minimize_changeover': True,
'max_batch_size': 10,
'time_window': 24,
'auto_optimization_enabled': False
}
# Benutzereinstellungen aus der Session laden oder Standardwerte verwenden
user_settings = session.get('user_settings', {})
optimization_settings = user_settings.get('optimization', default_settings)
# Sicherstellen, dass alle erforderlichen Schlüssel vorhanden sind
for key, value in default_settings.items():
if key not in optimization_settings:
optimization_settings[key] = value
return jsonify({
'success': True,
'settings': optimization_settings
})
except Exception as e:
app_logger.error(f"Fehler beim Abrufen der Optimierungs-Einstellungen: {str(e)}")
return jsonify({
'success': False,
'error': 'Fehler beim Laden der Einstellungen'
}), 500
elif request.method == 'POST':
try:
settings = request.get_json()
# Validierung der Einstellungen
if not validate_optimization_settings(settings):
return jsonify({
'success': False,
'error': 'Ungültige Optimierungs-Einstellungen'
}), 400
# Einstellungen in der Session speichern
user_settings = session.get('user_settings', {})
if 'optimization' not in user_settings:
user_settings['optimization'] = {}
# Aktualisiere die Optimierungseinstellungen
user_settings['optimization'].update(settings)
session['user_settings'] = user_settings
# Einstellungen in der Datenbank speichern, wenn möglich
if hasattr(current_user, 'settings'):
import json
current_user.settings = json.dumps(user_settings)
current_user.updated_at = datetime.now()
db_session.commit()
app_logger.info(f"Optimierungs-Einstellungen für Benutzer {current_user.id} aktualisiert")
return jsonify({
'success': True,
'message': 'Optimierungs-Einstellungen erfolgreich gespeichert'
})
except Exception as e:
db_session.rollback()
app_logger.error(f"Fehler beim Speichern der Optimierungs-Einstellungen: {str(e)}")
return jsonify({
'success': False,
'error': f'Fehler beim Speichern der Einstellungen: {str(e)}'
}), 500
# ===== OPTIMIERUNGS-ALGORITHMUS-FUNKTIONEN =====
def apply_round_robin_optimization(jobs, printers, db_session):
"""Round-Robin-Optimierung: Gleichmäßige Verteilung der Jobs auf Drucker"""
optimized_count = 0
printer_index = 0
for job in jobs:
if printer_index >= len(printers):
printer_index = 0
# Job dem nächsten Drucker zuweisen
job.printer_id = printers[printer_index].id
job.assigned_at = datetime.now()
optimized_count += 1
printer_index += 1
return optimized_count
def apply_load_balance_optimization(jobs, printers, db_session):
"""Load-Balancing-Optimierung: Jobs basierend auf aktueller Auslastung verteilen"""
optimized_count = 0
# Aktuelle Drucker-Auslastung berechnen
printer_loads = {}
for printer in printers:
current_jobs = db_session.query(Job).filter(
Job.printer_id == printer.id,
Job.status.in_(['running', 'queued'])
).count()
printer_loads[printer.id] = current_jobs
for job in jobs:
# Drucker mit geringster Auslastung finden
min_load_printer_id = min(printer_loads, key=printer_loads.get)
job.printer_id = min_load_printer_id
job.assigned_at = datetime.now()
# Auslastung für nächste Iteration aktualisieren
printer_loads[min_load_printer_id] += 1
optimized_count += 1
return optimized_count
def apply_priority_optimization(jobs, printers, db_session):
"""Prioritätsbasierte Optimierung: Jobs nach Priorität und verfügbaren Druckern verteilen"""
optimized_count = 0
# Jobs nach Priorität sortieren
priority_order = {'urgent': 1, 'high': 2, 'normal': 3, 'low': 4}
sorted_jobs = sorted(jobs, key=lambda j: priority_order.get(getattr(j, 'priority', 'normal'), 3))
# Hochpriorisierte Jobs den besten verfügbaren Druckern zuweisen
printer_assignments = {printer.id: 0 for printer in printers}
for job in sorted_jobs:
# Drucker mit geringster Anzahl zugewiesener Jobs finden
best_printer_id = min(printer_assignments, key=printer_assignments.get)
job.printer_id = best_printer_id
job.assigned_at = datetime.now()
printer_assignments[best_printer_id] += 1
optimized_count += 1
return optimized_count
def validate_optimization_settings(settings):
"""Validiert die Optimierungs-Einstellungen"""
try:
# Algorithmus validieren
valid_algorithms = ['round_robin', 'load_balance', 'priority_based']
if settings.get('algorithm') not in valid_algorithms:
return False
# Numerische Werte validieren
max_batch_size = settings.get('max_batch_size', 10)
if not isinstance(max_batch_size, int) or max_batch_size < 1 or max_batch_size > 50:
return False
time_window = settings.get('time_window', 24)
if not isinstance(time_window, int) or time_window < 1 or time_window > 168:
return False
return True
except Exception:
return False
# ===== ERWEITERTE REFRESH-FUNKTIONEN =====
@app.route('/api/dashboard/refresh', methods=['POST'])
@login_required
def refresh_dashboard():
"""Aktualisiert Dashboard-Daten und gibt aktuelle Statistiken zurück"""
try:
db_session = get_db_session()
# Aktuelle Statistiken abrufen
stats = {
'active_jobs': db_session.query(Job).filter(Job.status == 'running').count(),
'available_printers': db_session.query(Printer).filter(Printer.active == True).count(),
'total_jobs': db_session.query(Job).count(),
'pending_jobs': db_session.query(Job).filter(Job.status == 'queued').count()
}
# Erfolgsrate berechnen
total_jobs = stats['total_jobs']
if total_jobs > 0:
completed_jobs = db_session.query(Job).filter(Job.status == 'completed').count()
stats['success_rate'] = round((completed_jobs / total_jobs) * 100, 1)
else:
stats['success_rate'] = 0
db_session.close()
return jsonify({
'success': True,
'stats': stats,
'timestamp': datetime.now().isoformat()
})
except Exception as e:
app_logger.error(f"Fehler beim Dashboard-Refresh: {str(e)}")
return jsonify({
'success': False,
'error': 'Fehler beim Aktualisieren der Dashboard-Daten'
}), 500
# ===== ADMIN GASTAUFTRÄGE API-ENDPUNKTE =====
@app.route('/api/admin/guest-requests/test', methods=['GET'])
def test_admin_guest_requests():
"""Test-Endpunkt für Guest Requests Routing"""
app_logger.info("Test-Route /api/admin/guest-requests/test aufgerufen")
return jsonify({
'success': True,
'message': 'Test-Route funktioniert',
'user_authenticated': current_user.is_authenticated,
'user_is_admin': current_user.is_admin if current_user.is_authenticated else False
})
@app.route('/api/admin/guest-requests', methods=['GET'])
@admin_required
def get_admin_guest_requests():
"""Gibt alle Gastaufträge für Admin-Verwaltung zurück"""
try:
app_logger.info(f"API-Aufruf /api/admin/guest-requests von User {current_user.id if current_user.is_authenticated else 'Anonymous'}")
db_session = get_db_session()
# Parameter auslesen
status = request.args.get('status', 'all')
limit = int(request.args.get('limit', 50))
offset = int(request.args.get('offset', 0))
search = request.args.get('search', '')
# Basis-Query
query = db_session.query(GuestRequest)
# Status-Filter
if status != 'all':
query = query.filter(GuestRequest.status == status)
# Suchfilter
if search:
search_term = f"%{search}%"
query = query.filter(
(GuestRequest.name.ilike(search_term)) |
(GuestRequest.email.ilike(search_term)) |
(GuestRequest.file_name.ilike(search_term)) |
(GuestRequest.reason.ilike(search_term))
)
# Gesamtanzahl vor Pagination
total = query.count()
# Sortierung und Pagination
requests = query.order_by(GuestRequest.created_at.desc()).offset(offset).limit(limit).all()
# Statistiken berechnen
stats = {
'total': db_session.query(GuestRequest).count(),
'pending': db_session.query(GuestRequest).filter(GuestRequest.status == 'pending').count(),
'approved': db_session.query(GuestRequest).filter(GuestRequest.status == 'approved').count(),
'rejected': db_session.query(GuestRequest).filter(GuestRequest.status == 'rejected').count(),
}
# Requests zu Dictionary konvertieren
requests_data = []
for req in requests:
# Priorität berechnen
now = datetime.now()
hours_old = (now - req.created_at).total_seconds() / 3600 if req.created_at else 0
is_urgent = hours_old > 24 and req.status == 'pending'
request_data = {
'id': req.id,
'name': req.name,
'email': req.email,
'file_name': req.file_name,
'file_path': req.file_path,
'duration_minutes': req.duration_minutes,
'copies': req.copies,
'reason': req.reason,
'status': req.status,
'created_at': req.created_at.isoformat() if req.created_at else None,
'updated_at': req.updated_at.isoformat() if req.updated_at else None,
'approved_at': req.approved_at.isoformat() if req.approved_at else None,
'rejected_at': req.rejected_at.isoformat() if req.rejected_at else None,
'approval_notes': req.approval_notes,
'rejection_reason': req.rejection_reason,
'is_urgent': is_urgent,
'hours_old': round(hours_old, 1)
}
requests_data.append(request_data)
db_session.close()
app_logger.info(f"Admin-Gastaufträge geladen: {len(requests_data)} von {total} (Status: {status})")
return jsonify({
'success': True,
'requests': requests_data,
'stats': stats,
'total': total,
'offset': offset,
'limit': limit,
'has_more': offset + limit < total
})
except Exception as e:
app_logger.error(f"Fehler beim Laden der Admin-Gastaufträge: {str(e)}", exc_info=True)
return jsonify({
'success': False,
'message': f'Fehler beim Laden der Gastaufträge: {str(e)}'
}), 500
@app.route('/api/guest-requests/<int:request_id>/approve', methods=['POST'])
@admin_required
def approve_guest_request(request_id):
"""Genehmigt einen Gastauftrag"""
try:
db_session = get_db_session()
guest_request = db_session.query(GuestRequest).filter(GuestRequest.id == request_id).first()
if not guest_request:
db_session.close()
return jsonify({
'success': False,
'message': 'Gastauftrag nicht gefunden'
}), 404
if guest_request.status != 'pending':
db_session.close()
return jsonify({
'success': False,
'message': f'Gastauftrag kann im Status "{guest_request.status}" nicht genehmigt werden'
}), 400
# Daten aus Request Body
data = request.get_json() or {}
notes = data.get('notes', '')
printer_id = data.get('printer_id')
# Status aktualisieren
guest_request.status = 'approved'
guest_request.approved_at = datetime.now()
guest_request.approved_by = current_user.id
guest_request.approval_notes = notes
guest_request.updated_at = datetime.now()
# Falls Drucker zugewiesen werden soll
if printer_id:
printer = db_session.query(Printer).filter(Printer.id == printer_id).first()
if printer:
guest_request.assigned_printer_id = printer_id
# OTP-Code generieren für den Gast
import secrets
otp_code = ''.join([str(secrets.randbelow(10)) for _ in range(6)])
guest_request.otp_code = otp_code
guest_request.otp_expires_at = datetime.now() + timedelta(hours=24)
db_session.commit()
# Benachrichtigung an den Gast senden (falls E-Mail verfügbar)
if guest_request.email:
try:
# Hier würde normalerweise eine E-Mail gesendet werden
app_logger.info(f"E-Mail-Benachrichtigung würde an {guest_request.email} gesendet (OTP: {otp_code})")
except Exception as e:
app_logger.warning(f"Fehler beim Senden der E-Mail-Benachrichtigung: {str(e)}")
db_session.close()
app_logger.info(f"Gastauftrag {request_id} von Admin {current_user.id} genehmigt (OTP: {otp_code})")
return jsonify({
'success': True,
'message': 'Gastauftrag erfolgreich genehmigt',
'otp_code': otp_code,
'expires_at': (datetime.now() + timedelta(hours=24)).isoformat()
})
except Exception as e:
app_logger.error(f"Fehler beim Genehmigen des Gastauftrags {request_id}: {str(e)}")
return jsonify({
'success': False,
'message': f'Fehler beim Genehmigen: {str(e)}'
}), 500
@app.route('/api/guest-requests/<int:request_id>/reject', methods=['POST'])
@admin_required
def reject_guest_request(request_id):
"""Lehnt einen Gastauftrag ab"""
try:
db_session = get_db_session()
guest_request = db_session.query(GuestRequest).filter(GuestRequest.id == request_id).first()
if not guest_request:
db_session.close()
return jsonify({
'success': False,
'message': 'Gastauftrag nicht gefunden'
}), 404
if guest_request.status != 'pending':
db_session.close()
return jsonify({
'success': False,
'message': f'Gastauftrag kann im Status "{guest_request.status}" nicht abgelehnt werden'
}), 400
# Daten aus Request Body
data = request.get_json() or {}
reason = data.get('reason', '').strip()
if not reason:
db_session.close()
return jsonify({
'success': False,
'message': 'Ablehnungsgrund ist erforderlich'
}), 400
# Status aktualisieren
guest_request.status = 'rejected'
guest_request.rejected_at = datetime.now()
guest_request.rejected_by = current_user.id
guest_request.rejection_reason = reason
guest_request.updated_at = datetime.now()
db_session.commit()
# Benachrichtigung an den Gast senden (falls E-Mail verfügbar)
if guest_request.email:
try:
# Hier würde normalerweise eine E-Mail gesendet werden
app_logger.info(f"Ablehnungs-E-Mail würde an {guest_request.email} gesendet (Grund: {reason})")
except Exception as e:
app_logger.warning(f"Fehler beim Senden der Ablehnungs-E-Mail: {str(e)}")
db_session.close()
app_logger.info(f"Gastauftrag {request_id} von Admin {current_user.id} abgelehnt (Grund: {reason})")
return jsonify({
'success': True,
'message': 'Gastauftrag erfolgreich abgelehnt'
})
except Exception as e:
app_logger.error(f"Fehler beim Ablehnen des Gastauftrags {request_id}: {str(e)}")
return jsonify({
'success': False,
'message': f'Fehler beim Ablehnen: {str(e)}'
}), 500
@app.route('/api/guest-requests/<int:request_id>', methods=['DELETE'])
@admin_required
def delete_guest_request(request_id):
"""Löscht einen Gastauftrag"""
try:
db_session = get_db_session()
guest_request = db_session.query(GuestRequest).filter(GuestRequest.id == request_id).first()
if not guest_request:
db_session.close()
return jsonify({
'success': False,
'message': 'Gastauftrag nicht gefunden'
}), 404
# Datei löschen falls vorhanden
if guest_request.file_path and os.path.exists(guest_request.file_path):
try:
os.remove(guest_request.file_path)
app_logger.info(f"Datei {guest_request.file_path} für Gastauftrag {request_id} gelöscht")
except Exception as e:
app_logger.warning(f"Fehler beim Löschen der Datei: {str(e)}")
# Gastauftrag aus Datenbank löschen
request_name = guest_request.name
db_session.delete(guest_request)
db_session.commit()
db_session.close()
app_logger.info(f"Gastauftrag {request_id} ({request_name}) von Admin {current_user.id} gelöscht")
return jsonify({
'success': True,
'message': 'Gastauftrag erfolgreich gelöscht'
})
except Exception as e:
app_logger.error(f"Fehler beim Löschen des Gastauftrags {request_id}: {str(e)}")
return jsonify({
'success': False,
'message': f'Fehler beim Löschen: {str(e)}'
}), 500
@app.route('/api/guest-requests/<int:request_id>', methods=['GET'])
@admin_required
def get_guest_request_detail(request_id):
"""Gibt Details eines spezifischen Gastauftrags zurück"""
try:
db_session = get_db_session()
guest_request = db_session.query(GuestRequest).filter(GuestRequest.id == request_id).first()
if not guest_request:
db_session.close()
return jsonify({
'success': False,
'message': 'Gastauftrag nicht gefunden'
}), 404
# Detaildaten zusammenstellen
request_data = {
'id': guest_request.id,
'name': guest_request.name,
'email': guest_request.email,
'file_name': guest_request.file_name,
'file_path': guest_request.file_path,
'file_size': None,
'duration_minutes': guest_request.duration_minutes,
'copies': guest_request.copies,
'reason': guest_request.reason,
'status': guest_request.status,
'created_at': guest_request.created_at.isoformat() if guest_request.created_at else None,
'updated_at': guest_request.updated_at.isoformat() if guest_request.updated_at else None,
'approved_at': guest_request.approved_at.isoformat() if guest_request.approved_at else None,
'rejected_at': guest_request.rejected_at.isoformat() if guest_request.rejected_at else None,
'approval_notes': guest_request.approval_notes,
'rejection_reason': guest_request.rejection_reason,
'otp_code': guest_request.otp_code,
'otp_expires_at': guest_request.otp_expires_at.isoformat() if guest_request.otp_expires_at else None,
'author_ip': guest_request.author_ip
}
# Dateigröße ermitteln
if guest_request.file_path and os.path.exists(guest_request.file_path):
try:
file_size = os.path.getsize(guest_request.file_path)
request_data['file_size'] = file_size
request_data['file_size_mb'] = round(file_size / (1024 * 1024), 2)
except Exception as e:
app_logger.warning(f"Fehler beim Ermitteln der Dateigröße: {str(e)}")
# Bearbeiter-Informationen hinzufügen
if guest_request.approved_by:
approved_by_user = db_session.query(User).filter(User.id == guest_request.approved_by).first()
if approved_by_user:
request_data['approved_by_name'] = approved_by_user.name or approved_by_user.username
if guest_request.rejected_by:
rejected_by_user = db_session.query(User).filter(User.id == guest_request.rejected_by).first()
if rejected_by_user:
request_data['rejected_by_name'] = rejected_by_user.name or rejected_by_user.username
# Zugewiesener Drucker
if hasattr(guest_request, 'assigned_printer_id') and guest_request.assigned_printer_id:
assigned_printer = db_session.query(Printer).filter(Printer.id == guest_request.assigned_printer_id).first()
if assigned_printer:
request_data['assigned_printer'] = {
'id': assigned_printer.id,
'name': assigned_printer.name,
'location': assigned_printer.location,
'status': assigned_printer.status
}
db_session.close()
return jsonify({
'success': True,
'request': request_data
})
except Exception as e:
app_logger.error(f"Fehler beim Abrufen der Gastauftrag-Details {request_id}: {str(e)}")
return jsonify({
'success': False,
'message': f'Fehler beim Abrufen der Details: {str(e)}'
}), 500
@app.route('/api/admin/guest-requests/stats', methods=['GET'])
@admin_required
def get_guest_requests_stats():
"""Gibt detaillierte Statistiken zu Gastaufträgen zurück"""
try:
db_session = get_db_session()
# Basis-Statistiken
total = db_session.query(GuestRequest).count()
pending = db_session.query(GuestRequest).filter(GuestRequest.status == 'pending').count()
approved = db_session.query(GuestRequest).filter(GuestRequest.status == 'approved').count()
rejected = db_session.query(GuestRequest).filter(GuestRequest.status == 'rejected').count()
# Zeitbasierte Statistiken
today = datetime.now().date()
week_ago = datetime.now() - timedelta(days=7)
month_ago = datetime.now() - timedelta(days=30)
today_requests = db_session.query(GuestRequest).filter(
func.date(GuestRequest.created_at) == today
).count()
week_requests = db_session.query(GuestRequest).filter(
GuestRequest.created_at >= week_ago
).count()
month_requests = db_session.query(GuestRequest).filter(
GuestRequest.created_at >= month_ago
).count()
# Dringende Requests (älter als 24h und pending)
urgent_cutoff = datetime.now() - timedelta(hours=24)
urgent_requests = db_session.query(GuestRequest).filter(
GuestRequest.status == 'pending',
GuestRequest.created_at < urgent_cutoff
).count()
# Durchschnittliche Bearbeitungszeit
avg_processing_time = None
try:
processed_requests = db_session.query(GuestRequest).filter(
GuestRequest.status.in_(['approved', 'rejected']),
GuestRequest.updated_at.isnot(None)
).all()
if processed_requests:
total_time = sum([
(req.updated_at - req.created_at).total_seconds()
for req in processed_requests
if req.updated_at and req.created_at
])
avg_processing_time = round(total_time / len(processed_requests) / 3600, 2) # Stunden
except Exception as e:
app_logger.warning(f"Fehler beim Berechnen der durchschnittlichen Bearbeitungszeit: {str(e)}")
# Erfolgsrate
success_rate = 0
if approved + rejected > 0:
success_rate = round((approved / (approved + rejected)) * 100, 1)
stats = {
'total': total,
'pending': pending,
'approved': approved,
'rejected': rejected,
'urgent': urgent_requests,
'today': today_requests,
'week': week_requests,
'month': month_requests,
'success_rate': success_rate,
'avg_processing_time_hours': avg_processing_time,
'completion_rate': round(((approved + rejected) / total * 100), 1) if total > 0 else 0
}
db_session.close()
return jsonify({
'success': True,
'stats': stats,
'generated_at': datetime.now().isoformat()
})
except Exception as e:
app_logger.error(f"Fehler beim Abrufen der Gastauftrag-Statistiken: {str(e)}")
return jsonify({
'success': False,
'message': f'Fehler beim Abrufen der Statistiken: {str(e)}'
}), 500
@app.route('/api/admin/guest-requests/export', methods=['GET'])
@admin_required
def export_guest_requests():
"""Exportiert Gastaufträge als CSV"""
try:
db_session = get_db_session()
# Filter-Parameter
status = request.args.get('status', 'all')
start_date = request.args.get('start_date')
end_date = request.args.get('end_date')
# Query aufbauen
query = db_session.query(GuestRequest)
if status != 'all':
query = query.filter(GuestRequest.status == status)
if start_date:
try:
start_dt = datetime.fromisoformat(start_date)
query = query.filter(GuestRequest.created_at >= start_dt)
except ValueError:
pass
if end_date:
try:
end_dt = datetime.fromisoformat(end_date)
query = query.filter(GuestRequest.created_at <= end_dt)
except ValueError:
pass
requests = query.order_by(GuestRequest.created_at.desc()).all()
# CSV-Daten erstellen
import csv
import io
output = io.StringIO()
writer = csv.writer(output)
# Header
writer.writerow([
'ID', 'Name', 'E-Mail', 'Datei', 'Status', 'Erstellt am',
'Dauer (Min)', 'Kopien', 'Begründung', 'Genehmigt am',
'Abgelehnt am', 'Bearbeitungsnotizen', 'Ablehnungsgrund'
])
# Daten
for req in requests:
writer.writerow([
req.id,
req.name or '',
req.email or '',
req.file_name or '',
req.status,
req.created_at.strftime('%Y-%m-%d %H:%M:%S') if req.created_at else '',
req.duration_minutes or '',
req.copies or '',
req.reason or '',
req.approved_at.strftime('%Y-%m-%d %H:%M:%S') if req.approved_at else '',
req.rejected_at.strftime('%Y-%m-%d %H:%M:%S') if req.rejected_at else '',
req.approval_notes or '',
req.rejection_reason or ''
])
db_session.close()
# Response erstellen
output_value = output.getvalue()
output.close()
response = make_response(output_value)
response.headers["Content-Disposition"] = f"attachment; filename=gastauftraege_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
response.headers["Content-Type"] = "text/csv; charset=utf-8"
app_logger.info(f"Gastaufträge-Export erstellt: {len(requests)} Einträge")
return response
except Exception as e:
app_logger.error(f"Fehler beim Exportieren der Gastaufträge: {str(e)}")
return jsonify({
'success': False,
'message': f'Fehler beim Export: {str(e)}'
}), 500
# ===== ENDE ADMIN GASTAUFTRÄGE API-ENDPUNKTE =====
@app.route("/api/user/settings/auto-logout", methods=["GET"])
@login_required
def get_auto_logout_settings():
"""Holt nur die Auto-Logout-Einstellungen des Benutzers"""
try:
user_settings = session.get('user_settings', {})
auto_logout = user_settings.get('privacy', {}).get('auto_logout', 60)
return jsonify({
"success": True,
"auto_logout": auto_logout
})
except Exception as e:
user_logger.error(f"Fehler beim Laden der Auto-Logout-Einstellungen: {str(e)}")
return jsonify({
"success": False,
"error": "Fehler beim Laden der Auto-Logout-Einstellungen"
}), 500
@app.route("/api/user/setting", methods=["PATCH"])
@login_required
def update_single_setting():
"""Aktualisiert eine einzelne Benutzereinstellung"""
try:
if not request.is_json:
return jsonify({"error": "Anfrage muss im JSON-Format sein"}), 400
data = request.get_json()
if not data:
return jsonify({"error": "Keine Daten erhalten"}), 400
# Aktuelle Einstellungen laden
user_settings = session.get('user_settings', {
"theme": "system",
"reduced_motion": False,
"contrast": "normal",
"notifications": {
"new_jobs": True,
"job_updates": True,
"system": True,
"email": False
},
"privacy": {
"activity_logs": True,
"two_factor": False,
"auto_logout": 60
}
})
# Einzelne Einstellung aktualisieren
for key, value in data.items():
if key == "auto_logout":
# Validierung für Auto-Logout
try:
timeout = int(value) if value != "never" else "never"
if timeout != "never" and (timeout < 5 or timeout > 480):
return jsonify({"error": "Auto-Logout muss zwischen 5 und 480 Minuten liegen"}), 400
user_settings.setdefault('privacy', {})['auto_logout'] = timeout
except (ValueError, TypeError):
return jsonify({"error": "Ungültiger Auto-Logout-Wert"}), 400
user_settings['last_updated'] = datetime.now().isoformat()
session['user_settings'] = user_settings
user_logger.info(f"Benutzer {current_user.username} hat Einstellung '{key}' aktualisiert")
return jsonify({
"success": True,
"message": "Einstellung erfolgreich aktualisiert"
})
except Exception as e:
user_logger.error(f"Fehler beim Aktualisieren der Einzeleinstellung: {str(e)}")
return jsonify({
"success": False,
"error": "Fehler beim Aktualisieren der Einstellung"
}), 500
@app.route("/api/auth/keep-alive", methods=["POST"])
@login_required
def keep_alive():
"""Keep-Alive-Endpunkt für Auto-Logout-System"""
try:
# Session-Timestamp aktualisieren
session.permanent = True
session.modified = True
auth_logger.info(f"Keep-Alive für Benutzer {current_user.username}")
return jsonify({
"success": True,
"message": "Session verlängert",
"timestamp": datetime.now().isoformat()
})
except Exception as e:
auth_logger.error(f"Fehler beim Keep-Alive: {str(e)}")
return jsonify({
"success": False,
"error": "Fehler beim Verlängern der Session"
}), 500
# ===== STARTUP UND MAIN =====
if __name__ == "__main__":
import sys
import signal
import os
# Debug-Modus prüfen
debug_mode = len(sys.argv) > 1 and sys.argv[1] == "--debug"
# Windows-spezifische Umgebungsvariablen setzen für bessere Flask-Kompatibilität
if os.name == 'nt' and debug_mode:
# Entferne problematische Werkzeug-Variablen
os.environ.pop('WERKZEUG_SERVER_FD', None)
os.environ.pop('WERKZEUG_RUN_MAIN', None)
# Setze saubere Umgebung
os.environ['FLASK_ENV'] = 'development'
os.environ['PYTHONIOENCODING'] = 'utf-8'
os.environ['PYTHONUTF8'] = '1'
# Windows-spezifisches Signal-Handling für ordnungsgemäßes Shutdown
def signal_handler(sig, frame):
"""Signal-Handler für ordnungsgemäßes Shutdown."""
app_logger.warning(f"🛑 Signal {sig} empfangen - fahre System herunter...")
try:
# Queue Manager stoppen
app_logger.info("🔄 Beende Queue Manager...")
stop_queue_manager()
# Scheduler stoppen falls aktiviert
if SCHEDULER_ENABLED and scheduler:
try:
scheduler.stop()
app_logger.info("Job-Scheduler gestoppt")
except Exception as e:
app_logger.error(f"Fehler beim Stoppen des Schedulers: {str(e)}")
app_logger.info("✅ Shutdown abgeschlossen")
sys.exit(0)
except Exception as e:
app_logger.error(f"❌ Fehler beim Shutdown: {str(e)}")
sys.exit(1)
# Signal-Handler registrieren (Windows-kompatibel)
if os.name == 'nt': # Windows
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
# Zusätzlich für Flask-Development-Server
signal.signal(signal.SIGBREAK, signal_handler)
else: # Unix/Linux
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGHUP, signal_handler)
try:
# Datenbank initialisieren
init_database()
create_initial_admin()
# Template-Hilfsfunktionen registrieren
register_template_helpers(app)
# Queue-Manager für automatische Drucker-Überwachung starten
# Nur im Produktionsmodus starten (nicht im Debug-Modus)
if not debug_mode:
try:
queue_manager = start_queue_manager()
app_logger.info("✅ Printer Queue Manager erfolgreich gestartet")
# Verbesserte Shutdown-Handler registrieren
def cleanup_queue_manager():
try:
app_logger.info("🔄 Beende Queue Manager...")
stop_queue_manager()
except Exception as e:
app_logger.error(f"❌ Fehler beim Queue Manager Cleanup: {str(e)}")
atexit.register(cleanup_queue_manager)
except Exception as e:
app_logger.error(f"❌ Fehler beim Starten des Queue-Managers: {str(e)}")
else:
app_logger.info("🔄 Debug-Modus: Queue Manager deaktiviert für Entwicklung")
# Scheduler starten (falls aktiviert)
if SCHEDULER_ENABLED:
try:
scheduler.start()
app_logger.info("Job-Scheduler gestartet")
except Exception as e:
app_logger.error(f"Fehler beim Starten des Schedulers: {str(e)}")
if debug_mode:
# Debug-Modus: HTTP auf Port 5000
app_logger.info("Starte Debug-Server auf 0.0.0.0:5000 (HTTP)")
# Windows-spezifische Flask-Konfiguration
run_kwargs = {
"host": "0.0.0.0",
"port": 5000,
"debug": True,
"threaded": True
}
if os.name == 'nt':
# Windows: Deaktiviere Auto-Reload um WERKZEUG_SERVER_FD Fehler zu vermeiden
run_kwargs["use_reloader"] = False
run_kwargs["passthrough_errors"] = False
app_logger.info("Windows-Debug-Modus: Auto-Reload deaktiviert")
app.run(**run_kwargs)
else:
# Produktions-Modus: HTTPS auf Port 443
ssl_context = get_ssl_context()
if ssl_context:
app_logger.info("Starte HTTPS-Server auf 0.0.0.0:443")
app.run(
host="0.0.0.0",
port=443,
debug=False,
ssl_context=ssl_context,
threaded=True
)
else:
app_logger.info("Starte HTTP-Server auf 0.0.0.0:8080")
app.run(
host="0.0.0.0",
port=8080,
debug=False,
threaded=True
)
except KeyboardInterrupt:
app_logger.info("🔄 Tastatur-Unterbrechung empfangen - beende Anwendung...")
signal_handler(signal.SIGINT, None)
except Exception as e:
app_logger.error(f"Fehler beim Starten der Anwendung: {str(e)}")
# Cleanup bei Fehler
try:
stop_queue_manager()
except:
pass
sys.exit(1)