"feat: Integrate new charting library in frontend"

This commit is contained in:
Till Tomczak 2025-05-29 21:43:27 +02:00
parent 919ebc312e
commit aea600ee2e
2 changed files with 899 additions and 258 deletions

View File

@ -822,7 +822,7 @@ def user_update_profile():
@login_required @login_required
def user_api_update_settings(): def user_api_update_settings():
"""API-Endpunkt für Einstellungen-Updates (JSON)""" """API-Endpunkt für Einstellungen-Updates (JSON)"""
return user_update_settings() return user_update_profile()
@app.route("/user/update-settings", methods=["POST"]) @app.route("/user/update-settings", methods=["POST"])
@login_required @login_required
@ -2743,6 +2743,223 @@ def get_stats():
app_logger.error(f"Fehler beim Abrufen der Statistiken: {str(e)}") app_logger.error(f"Fehler beim Abrufen der Statistiken: {str(e)}")
return jsonify({"error": "Interner Serverfehler"}), 500 return jsonify({"error": "Interner Serverfehler"}), 500
@app.route("/api/stats/charts/job-status", methods=["GET"])
@login_required
def get_job_status_chart_data():
"""Gibt Diagrammdaten für Job-Status-Verteilung zurück."""
try:
db_session = get_db_session()
# Job-Status zählen
job_status_counts = {
'completed': db_session.query(Job).filter(Job.status == 'completed').count(),
'failed': db_session.query(Job).filter(Job.status == 'failed').count(),
'cancelled': db_session.query(Job).filter(Job.status == 'cancelled').count(),
'running': db_session.query(Job).filter(Job.status == 'running').count(),
'scheduled': db_session.query(Job).filter(Job.status == 'scheduled').count()
}
db_session.close()
chart_data = {
'labels': ['Abgeschlossen', 'Fehlgeschlagen', 'Abgebrochen', 'Läuft', 'Geplant'],
'datasets': [{
'label': 'Anzahl Jobs',
'data': [
job_status_counts['completed'],
job_status_counts['failed'],
job_status_counts['cancelled'],
job_status_counts['running'],
job_status_counts['scheduled']
],
'backgroundColor': [
'#10b981', # Grün für abgeschlossen
'#ef4444', # Rot für fehlgeschlagen
'#6b7280', # Grau für abgebrochen
'#3b82f6', # Blau für läuft
'#f59e0b' # Orange für geplant
]
}]
}
return jsonify(chart_data)
except Exception as e:
app_logger.error(f"Fehler beim Abrufen der Job-Status-Diagrammdaten: {str(e)}")
return jsonify({"error": "Interner Serverfehler"}), 500
@app.route("/api/stats/charts/printer-usage", methods=["GET"])
@login_required
def get_printer_usage_chart_data():
"""Gibt Diagrammdaten für Drucker-Nutzung zurück."""
try:
db_session = get_db_session()
# Drucker mit Job-Anzahl
printer_usage = db_session.query(
Printer.name,
func.count(Job.id).label('job_count')
).outerjoin(Job).group_by(Printer.id, Printer.name).all()
db_session.close()
chart_data = {
'labels': [usage[0] for usage in printer_usage],
'datasets': [{
'label': 'Anzahl Jobs',
'data': [usage[1] for usage in printer_usage],
'backgroundColor': '#3b82f6',
'borderColor': '#1d4ed8',
'borderWidth': 1
}]
}
return jsonify(chart_data)
except Exception as e:
app_logger.error(f"Fehler beim Abrufen der Drucker-Nutzung-Diagrammdaten: {str(e)}")
return jsonify({"error": "Interner Serverfehler"}), 500
@app.route("/api/stats/charts/jobs-timeline", methods=["GET"])
@login_required
def get_jobs_timeline_chart_data():
"""Gibt Diagrammdaten für Jobs-Timeline der letzten 30 Tage zurück."""
try:
db_session = get_db_session()
# Letzte 30 Tage
end_date = datetime.now().date()
start_date = end_date - timedelta(days=30)
# Jobs pro Tag der letzten 30 Tage
daily_jobs = db_session.query(
func.date(Job.created_at).label('date'),
func.count(Job.id).label('count')
).filter(
func.date(Job.created_at) >= start_date,
func.date(Job.created_at) <= end_date
).group_by(func.date(Job.created_at)).all()
# Alle Tage füllen (auch ohne Jobs)
date_dict = {job_date: count for job_date, count in daily_jobs}
labels = []
data = []
current_date = start_date
while current_date <= end_date:
labels.append(current_date.strftime('%d.%m'))
data.append(date_dict.get(current_date, 0))
current_date += timedelta(days=1)
db_session.close()
chart_data = {
'labels': labels,
'datasets': [{
'label': 'Jobs pro Tag',
'data': data,
'fill': True,
'backgroundColor': 'rgba(59, 130, 246, 0.1)',
'borderColor': '#3b82f6',
'tension': 0.4
}]
}
return jsonify(chart_data)
except Exception as e:
app_logger.error(f"Fehler beim Abrufen der Jobs-Timeline-Diagrammdaten: {str(e)}")
return jsonify({"error": "Interner Serverfehler"}), 500
@app.route("/api/stats/charts/user-activity", methods=["GET"])
@login_required
def get_user_activity_chart_data():
"""Gibt Diagrammdaten für Top-Benutzer-Aktivität zurück."""
try:
db_session = get_db_session()
# Top 10 Benutzer nach Job-Anzahl
top_users = db_session.query(
User.username,
func.count(Job.id).label('job_count')
).join(Job).group_by(
User.id, User.username
).order_by(
func.count(Job.id).desc()
).limit(10).all()
db_session.close()
chart_data = {
'labels': [user[0] for user in top_users],
'datasets': [{
'label': 'Anzahl Jobs',
'data': [user[1] for user in top_users],
'backgroundColor': '#8b5cf6',
'borderColor': '#7c3aed',
'borderWidth': 1
}]
}
return jsonify(chart_data)
except Exception as e:
app_logger.error(f"Fehler beim Abrufen der Benutzer-Aktivität-Diagrammdaten: {str(e)}")
return jsonify({"error": "Interner Serverfehler"}), 500
@app.route("/api/stats/export", methods=["GET"])
@login_required
def export_stats():
"""Exportiert Statistiken als CSV."""
try:
db_session = get_db_session()
# Basis-Statistiken sammeln
total_users = db_session.query(User).count()
total_printers = db_session.query(Printer).count()
total_jobs = db_session.query(Job).count()
completed_jobs = db_session.query(Job).filter(Job.status == "completed").count()
failed_jobs = db_session.query(Job).filter(Job.status == "failed").count()
# CSV-Inhalt erstellen
import io
import csv
output = io.StringIO()
writer = csv.writer(output)
# Header
writer.writerow(['Metrik', 'Wert'])
# Daten
writer.writerow(['Gesamte Benutzer', total_users])
writer.writerow(['Gesamte Drucker', total_printers])
writer.writerow(['Gesamte Jobs', total_jobs])
writer.writerow(['Abgeschlossene Jobs', completed_jobs])
writer.writerow(['Fehlgeschlagene Jobs', failed_jobs])
writer.writerow(['Erfolgsrate (%)', round((completed_jobs / total_jobs * 100), 2) if total_jobs > 0 else 0])
writer.writerow(['Exportiert am', datetime.now().strftime('%d.%m.%Y %H:%M:%S')])
db_session.close()
# Response vorbereiten
output.seek(0)
response = Response(
output.getvalue(),
mimetype='text/csv',
headers={
'Content-Disposition': f'attachment; filename=statistiken_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv'
}
)
return response
except Exception as e:
app_logger.error(f"Fehler beim Exportieren der Statistiken: {str(e)}")
return jsonify({"error": "Interner Serverfehler"}), 500
@app.route("/api/admin/users", methods=["GET"]) @app.route("/api/admin/users", methods=["GET"])
@login_required @login_required
def get_users(): def get_users():
@ -3895,137 +4112,57 @@ def admin_update_user_form(user_id):
def admin_update_printer_form(printer_id): def admin_update_printer_form(printer_id):
"""Aktualisiert einen Drucker über HTML-Form (nur für Admins).""" """Aktualisiert einen Drucker über HTML-Form (nur für Admins)."""
if not current_user.is_admin: if not current_user.is_admin:
import os flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error")
import sys return redirect(url_for("index"))
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
from contextlib import contextmanager
# 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, TAPO_USERNAME, TAPO_PASSWORD
from utils.file_manager import file_manager, save_job_file, save_guest_file, save_avatar_file, delete_file as delete_file_safe
# 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: try:
from utils.backup_manager import BackupManager # Form-Daten lesen
backup_manager = BackupManager() name = request.form.get("name", "").strip()
except ImportError: ip_address = request.form.get("ip_address", "").strip()
backup_manager = None 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()
# Import neuer Systeme # Pflichtfelder prüfen
from utils.rate_limiter import limit_requests, rate_limiter, cleanup_rate_limiter if not name or not ip_address:
from utils.security import init_security, require_secure_headers, security_check flash("Name und IP-Adresse sind erforderlich.", "error")
from utils.permissions import init_permission_helpers, require_permission, Permission, check_permission return redirect(url_for("admin_edit_printer_page", printer_id=printer_id))
from utils.analytics import analytics_engine, track_event, get_dashboard_stats
# Drucker-Monitor importieren # IP-Adresse validieren
from utils.printer_monitor import printer_monitor 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_edit_printer_page", printer_id=printer_id))
# Flask-App initialisieren db_session = get_db_session()
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 printer = db_session.query(Printer).get(printer_id)
csrf = CSRFProtect(app) if not printer:
db_session.close()
flash("Drucker nicht gefunden.", "error")
return redirect(url_for("admin_page", tab="printers"))
# Security-System initialisieren # Drucker aktualisieren
app = init_security(app) printer.name = name
printer.model = model
printer.location = location
printer.description = description
printer.plug_ip = ip_address
printer.status = status
# Permission Template Helpers registrieren db_session.commit()
init_permission_helpers(app) db_session.close()
# Template-Helper registrieren printers_logger.info(f"Drucker '{printer.name}' (ID: {printer_id}) aktualisiert von Admin {current_user.id}")
register_template_helpers(app) flash(f"Drucker '{printer.name}' erfolgreich aktualisiert.", "success")
return redirect(url_for("admin_page", tab="printers"))
# CSRF-Error-Handler - Korrigierte Version für Flask-WTF 1.2.1+ except Exception as e:
@app.errorhandler(CSRFError) printers_logger.error(f"Fehler beim Aktualisieren eines Druckers über Form: {str(e)}")
def csrf_error(error): flash("Fehler beim Aktualisieren des Druckers.", "error")
"""Behandelt CSRF-Fehler und gibt detaillierte Informationen zurück.""" return redirect(url_for("admin_edit_printer_page", printer_id=printer_id))
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 initialisieren
login_manager = LoginManager() login_manager = LoginManager()
@ -4719,7 +4856,7 @@ def user_update_profile():
@login_required @login_required
def user_api_update_settings(): def user_api_update_settings():
"""API-Endpunkt für Einstellungen-Updates (JSON)""" """API-Endpunkt für Einstellungen-Updates (JSON)"""
return user_update_settings() return user_update_profile()
@app.route("/user/update-settings", methods=["POST"]) @app.route("/user/update-settings", methods=["POST"])
@login_required @login_required
@ -6640,6 +6777,223 @@ def get_stats():
app_logger.error(f"Fehler beim Abrufen der Statistiken: {str(e)}") app_logger.error(f"Fehler beim Abrufen der Statistiken: {str(e)}")
return jsonify({"error": "Interner Serverfehler"}), 500 return jsonify({"error": "Interner Serverfehler"}), 500
@app.route("/api/stats/charts/job-status", methods=["GET"])
@login_required
def get_job_status_chart_data():
"""Gibt Diagrammdaten für Job-Status-Verteilung zurück."""
try:
db_session = get_db_session()
# Job-Status zählen
job_status_counts = {
'completed': db_session.query(Job).filter(Job.status == 'completed').count(),
'failed': db_session.query(Job).filter(Job.status == 'failed').count(),
'cancelled': db_session.query(Job).filter(Job.status == 'cancelled').count(),
'running': db_session.query(Job).filter(Job.status == 'running').count(),
'scheduled': db_session.query(Job).filter(Job.status == 'scheduled').count()
}
db_session.close()
chart_data = {
'labels': ['Abgeschlossen', 'Fehlgeschlagen', 'Abgebrochen', 'Läuft', 'Geplant'],
'datasets': [{
'label': 'Anzahl Jobs',
'data': [
job_status_counts['completed'],
job_status_counts['failed'],
job_status_counts['cancelled'],
job_status_counts['running'],
job_status_counts['scheduled']
],
'backgroundColor': [
'#10b981', # Grün für abgeschlossen
'#ef4444', # Rot für fehlgeschlagen
'#6b7280', # Grau für abgebrochen
'#3b82f6', # Blau für läuft
'#f59e0b' # Orange für geplant
]
}]
}
return jsonify(chart_data)
except Exception as e:
app_logger.error(f"Fehler beim Abrufen der Job-Status-Diagrammdaten: {str(e)}")
return jsonify({"error": "Interner Serverfehler"}), 500
@app.route("/api/stats/charts/printer-usage", methods=["GET"])
@login_required
def get_printer_usage_chart_data():
"""Gibt Diagrammdaten für Drucker-Nutzung zurück."""
try:
db_session = get_db_session()
# Drucker mit Job-Anzahl
printer_usage = db_session.query(
Printer.name,
func.count(Job.id).label('job_count')
).outerjoin(Job).group_by(Printer.id, Printer.name).all()
db_session.close()
chart_data = {
'labels': [usage[0] for usage in printer_usage],
'datasets': [{
'label': 'Anzahl Jobs',
'data': [usage[1] for usage in printer_usage],
'backgroundColor': '#3b82f6',
'borderColor': '#1d4ed8',
'borderWidth': 1
}]
}
return jsonify(chart_data)
except Exception as e:
app_logger.error(f"Fehler beim Abrufen der Drucker-Nutzung-Diagrammdaten: {str(e)}")
return jsonify({"error": "Interner Serverfehler"}), 500
@app.route("/api/stats/charts/jobs-timeline", methods=["GET"])
@login_required
def get_jobs_timeline_chart_data():
"""Gibt Diagrammdaten für Jobs-Timeline der letzten 30 Tage zurück."""
try:
db_session = get_db_session()
# Letzte 30 Tage
end_date = datetime.now().date()
start_date = end_date - timedelta(days=30)
# Jobs pro Tag der letzten 30 Tage
daily_jobs = db_session.query(
func.date(Job.created_at).label('date'),
func.count(Job.id).label('count')
).filter(
func.date(Job.created_at) >= start_date,
func.date(Job.created_at) <= end_date
).group_by(func.date(Job.created_at)).all()
# Alle Tage füllen (auch ohne Jobs)
date_dict = {job_date: count for job_date, count in daily_jobs}
labels = []
data = []
current_date = start_date
while current_date <= end_date:
labels.append(current_date.strftime('%d.%m'))
data.append(date_dict.get(current_date, 0))
current_date += timedelta(days=1)
db_session.close()
chart_data = {
'labels': labels,
'datasets': [{
'label': 'Jobs pro Tag',
'data': data,
'fill': True,
'backgroundColor': 'rgba(59, 130, 246, 0.1)',
'borderColor': '#3b82f6',
'tension': 0.4
}]
}
return jsonify(chart_data)
except Exception as e:
app_logger.error(f"Fehler beim Abrufen der Jobs-Timeline-Diagrammdaten: {str(e)}")
return jsonify({"error": "Interner Serverfehler"}), 500
@app.route("/api/stats/charts/user-activity", methods=["GET"])
@login_required
def get_user_activity_chart_data():
"""Gibt Diagrammdaten für Top-Benutzer-Aktivität zurück."""
try:
db_session = get_db_session()
# Top 10 Benutzer nach Job-Anzahl
top_users = db_session.query(
User.username,
func.count(Job.id).label('job_count')
).join(Job).group_by(
User.id, User.username
).order_by(
func.count(Job.id).desc()
).limit(10).all()
db_session.close()
chart_data = {
'labels': [user[0] for user in top_users],
'datasets': [{
'label': 'Anzahl Jobs',
'data': [user[1] for user in top_users],
'backgroundColor': '#8b5cf6',
'borderColor': '#7c3aed',
'borderWidth': 1
}]
}
return jsonify(chart_data)
except Exception as e:
app_logger.error(f"Fehler beim Abrufen der Benutzer-Aktivität-Diagrammdaten: {str(e)}")
return jsonify({"error": "Interner Serverfehler"}), 500
@app.route("/api/stats/export", methods=["GET"])
@login_required
def export_stats():
"""Exportiert Statistiken als CSV."""
try:
db_session = get_db_session()
# Basis-Statistiken sammeln
total_users = db_session.query(User).count()
total_printers = db_session.query(Printer).count()
total_jobs = db_session.query(Job).count()
completed_jobs = db_session.query(Job).filter(Job.status == "completed").count()
failed_jobs = db_session.query(Job).filter(Job.status == "failed").count()
# CSV-Inhalt erstellen
import io
import csv
output = io.StringIO()
writer = csv.writer(output)
# Header
writer.writerow(['Metrik', 'Wert'])
# Daten
writer.writerow(['Gesamte Benutzer', total_users])
writer.writerow(['Gesamte Drucker', total_printers])
writer.writerow(['Gesamte Jobs', total_jobs])
writer.writerow(['Abgeschlossene Jobs', completed_jobs])
writer.writerow(['Fehlgeschlagene Jobs', failed_jobs])
writer.writerow(['Erfolgsrate (%)', round((completed_jobs / total_jobs * 100), 2) if total_jobs > 0 else 0])
writer.writerow(['Exportiert am', datetime.now().strftime('%d.%m.%Y %H:%M:%S')])
db_session.close()
# Response vorbereiten
output.seek(0)
response = Response(
output.getvalue(),
mimetype='text/csv',
headers={
'Content-Disposition': f'attachment; filename=statistiken_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv'
}
)
return response
except Exception as e:
app_logger.error(f"Fehler beim Exportieren der Statistiken: {str(e)}")
return jsonify({"error": "Interner Serverfehler"}), 500
@app.route("/api/admin/users", methods=["GET"]) @app.route("/api/admin/users", methods=["GET"])
@login_required @login_required
def get_users(): def get_users():
@ -9158,132 +9512,6 @@ def auto_optimize_jobs():
'error': f'Optimierung fehlgeschlagen: {str(e)}' 'error': f'Optimierung fehlgeschlagen: {str(e)}'
}), 500 }), 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']) @app.route('/api/optimization/settings', methods=['GET', 'POST'])
@login_required @login_required
def optimization_settings(): def optimization_settings():

View File

@ -0,0 +1,413 @@
/**
* Charts.js - Diagramm-Management mit Chart.js für MYP Platform
*
* Verwaltet alle Diagramme auf der Statistiken-Seite.
* Unterstützt Dark Mode und Live-Updates.
*/
// Chart.js Instanzen Global verfügbar machen
window.statsCharts = {};
// Chart.js Konfiguration für Dark/Light Theme
function getChartTheme() {
const isDark = document.documentElement.classList.contains('dark');
return {
isDark: isDark,
backgroundColor: isDark ? 'rgba(30, 41, 59, 0.8)' : 'rgba(255, 255, 255, 0.8)',
textColor: isDark ? '#e2e8f0' : '#374151',
gridColor: isDark ? 'rgba(148, 163, 184, 0.1)' : 'rgba(156, 163, 175, 0.2)',
borderColor: isDark ? 'rgba(148, 163, 184, 0.3)' : 'rgba(156, 163, 175, 0.5)'
};
}
// Standard Chart.js Optionen
function getDefaultChartOptions() {
const theme = getChartTheme();
return {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: theme.textColor,
font: {
family: 'Inter, sans-serif'
}
}
},
tooltip: {
backgroundColor: theme.backgroundColor,
titleColor: theme.textColor,
bodyColor: theme.textColor,
borderColor: theme.borderColor,
borderWidth: 1
}
},
scales: {
x: {
ticks: {
color: theme.textColor,
font: {
family: 'Inter, sans-serif'
}
},
grid: {
color: theme.gridColor
}
},
y: {
ticks: {
color: theme.textColor,
font: {
family: 'Inter, sans-serif'
}
},
grid: {
color: theme.gridColor
}
}
}
};
}
// Job Status Doughnut Chart
async function createJobStatusChart() {
try {
const response = await fetch('/api/stats/charts/job-status');
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Fehler beim Laden der Job-Status-Daten');
}
const ctx = document.getElementById('job-status-chart');
if (!ctx) return;
// Vorhandenes Chart zerstören falls vorhanden
if (window.statsCharts.jobStatus) {
window.statsCharts.jobStatus.destroy();
}
const theme = getChartTheme();
window.statsCharts.jobStatus = new Chart(ctx, {
type: 'doughnut',
data: data,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: {
color: theme.textColor,
font: {
family: 'Inter, sans-serif',
size: 12
},
padding: 15
}
},
tooltip: {
backgroundColor: theme.backgroundColor,
titleColor: theme.textColor,
bodyColor: theme.textColor,
borderColor: theme.borderColor,
borderWidth: 1,
callbacks: {
label: function(context) {
const label = context.label || '';
const value = context.parsed;
const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage = total > 0 ? Math.round((value / total) * 100) : 0;
return `${label}: ${value} (${percentage}%)`;
}
}
}
},
cutout: '60%'
}
});
} catch (error) {
console.error('Fehler beim Erstellen des Job-Status-Charts:', error);
showChartError('job-status-chart', 'Fehler beim Laden der Job-Status-Daten');
}
}
// Drucker-Nutzung Bar Chart
async function createPrinterUsageChart() {
try {
const response = await fetch('/api/stats/charts/printer-usage');
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Fehler beim Laden der Drucker-Nutzung-Daten');
}
const ctx = document.getElementById('printer-usage-chart');
if (!ctx) return;
// Vorhandenes Chart zerstören falls vorhanden
if (window.statsCharts.printerUsage) {
window.statsCharts.printerUsage.destroy();
}
const options = getDefaultChartOptions();
options.scales.y.title = {
display: true,
text: 'Anzahl Jobs',
color: getChartTheme().textColor,
font: {
family: 'Inter, sans-serif'
}
};
window.statsCharts.printerUsage = new Chart(ctx, {
type: 'bar',
data: data,
options: options
});
} catch (error) {
console.error('Fehler beim Erstellen des Drucker-Nutzung-Charts:', error);
showChartError('printer-usage-chart', 'Fehler beim Laden der Drucker-Nutzung-Daten');
}
}
// Jobs Timeline Line Chart
async function createJobsTimelineChart() {
try {
const response = await fetch('/api/stats/charts/jobs-timeline');
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Fehler beim Laden der Jobs-Timeline-Daten');
}
const ctx = document.getElementById('jobs-timeline-chart');
if (!ctx) return;
// Vorhandenes Chart zerstören falls vorhanden
if (window.statsCharts.jobsTimeline) {
window.statsCharts.jobsTimeline.destroy();
}
const options = getDefaultChartOptions();
options.scales.y.title = {
display: true,
text: 'Jobs pro Tag',
color: getChartTheme().textColor,
font: {
family: 'Inter, sans-serif'
}
};
options.scales.x.title = {
display: true,
text: 'Datum (letzte 30 Tage)',
color: getChartTheme().textColor,
font: {
family: 'Inter, sans-serif'
}
};
window.statsCharts.jobsTimeline = new Chart(ctx, {
type: 'line',
data: data,
options: options
});
} catch (error) {
console.error('Fehler beim Erstellen des Jobs-Timeline-Charts:', error);
showChartError('jobs-timeline-chart', 'Fehler beim Laden der Jobs-Timeline-Daten');
}
}
// Benutzer-Aktivität Bar Chart
async function createUserActivityChart() {
try {
const response = await fetch('/api/stats/charts/user-activity');
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Fehler beim Laden der Benutzer-Aktivität-Daten');
}
const ctx = document.getElementById('user-activity-chart');
if (!ctx) return;
// Vorhandenes Chart zerstören falls vorhanden
if (window.statsCharts.userActivity) {
window.statsCharts.userActivity.destroy();
}
const options = getDefaultChartOptions();
options.indexAxis = 'y'; // Horizontales Balkendiagramm
options.scales.x.title = {
display: true,
text: 'Anzahl Jobs',
color: getChartTheme().textColor,
font: {
family: 'Inter, sans-serif'
}
};
options.scales.y.title = {
display: true,
text: 'Benutzer',
color: getChartTheme().textColor,
font: {
family: 'Inter, sans-serif'
}
};
window.statsCharts.userActivity = new Chart(ctx, {
type: 'bar',
data: data,
options: options
});
} catch (error) {
console.error('Fehler beim Erstellen des Benutzer-Aktivität-Charts:', error);
showChartError('user-activity-chart', 'Fehler beim Laden der Benutzer-Aktivität-Daten');
}
}
// Fehleranzeige in Chart-Container
function showChartError(chartId, message) {
const container = document.getElementById(chartId);
if (container) {
container.innerHTML = `
<div class="flex items-center justify-center h-full">
<div class="text-center">
<svg class="h-12 w-12 mx-auto text-red-500 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L4.268 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
<p class="text-red-500 font-medium">${message}</p>
<button onclick="refreshAllCharts()" class="mt-2 text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">
Erneut versuchen
</button>
</div>
</div>
`;
}
}
// Alle Charts erstellen
async function initializeAllCharts() {
// Loading-Indikatoren anzeigen
showChartLoading();
// Charts parallel erstellen
await Promise.allSettled([
createJobStatusChart(),
createPrinterUsageChart(),
createJobsTimelineChart(),
createUserActivityChart()
]);
}
// Loading-Indikatoren anzeigen
function showChartLoading() {
const chartIds = ['job-status-chart', 'printer-usage-chart', 'jobs-timeline-chart', 'user-activity-chart'];
chartIds.forEach(chartId => {
const container = document.getElementById(chartId);
if (container) {
container.innerHTML = `
<div class="flex items-center justify-center h-full">
<div class="text-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2"></div>
<p class="text-slate-500 dark:text-slate-400 text-sm">Diagramm wird geladen...</p>
</div>
</div>
`;
}
});
}
// Alle Charts aktualisieren
async function refreshAllCharts() {
console.log('Aktualisiere alle Diagramme...');
// Bestehende Charts zerstören
Object.values(window.statsCharts).forEach(chart => {
if (chart && typeof chart.destroy === 'function') {
chart.destroy();
}
});
// Charts neu erstellen
await initializeAllCharts();
console.log('Alle Diagramme aktualisiert');
}
// Theme-Wechsel handhaben
function updateChartsTheme() {
// Alle Charts mit neuem Theme aktualisieren
refreshAllCharts();
}
// Auto-refresh (alle 5 Minuten)
let chartRefreshInterval;
function startChartAutoRefresh() {
// Bestehenden Interval stoppen
if (chartRefreshInterval) {
clearInterval(chartRefreshInterval);
}
// Neuen Interval starten (5 Minuten)
chartRefreshInterval = setInterval(() => {
refreshAllCharts();
}, 5 * 60 * 1000);
}
function stopChartAutoRefresh() {
if (chartRefreshInterval) {
clearInterval(chartRefreshInterval);
chartRefreshInterval = null;
}
}
// Cleanup beim Verlassen der Seite
function cleanup() {
stopChartAutoRefresh();
// Alle Charts zerstören
Object.values(window.statsCharts).forEach(chart => {
if (chart && typeof chart.destroy === 'function') {
chart.destroy();
}
});
window.statsCharts = {};
}
// Globale Funktionen verfügbar machen
window.refreshAllCharts = refreshAllCharts;
window.updateChartsTheme = updateChartsTheme;
window.startChartAutoRefresh = startChartAutoRefresh;
window.stopChartAutoRefresh = stopChartAutoRefresh;
window.cleanup = cleanup;
// Event Listeners
document.addEventListener('DOMContentLoaded', function() {
// Charts initialisieren wenn auf Stats-Seite
if (document.getElementById('job-status-chart')) {
initializeAllCharts();
startChartAutoRefresh();
}
});
// Dark Mode Event Listener
if (typeof window.addEventListener !== 'undefined') {
window.addEventListener('darkModeChanged', function(e) {
updateChartsTheme();
});
}
// Page unload cleanup
window.addEventListener('beforeunload', cleanup);