1161 lines
43 KiB
Python
1161 lines
43 KiB
Python
"""
|
|
Echtzeit-Dashboard-System für das MYP-System
|
|
==========================================
|
|
|
|
Dieses Modul stellt ein erweiterte Dashboard mit Echtzeit-Updates bereit:
|
|
- WebSocket-basierte Live-Updates
|
|
- Ereignis-gesteuerte Benachrichtigungen
|
|
- Modulare Dashboard-Widgets
|
|
- Performance-optimierte Datenabfragen
|
|
- Responsive Design mit automatischer Anpassung
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
import time
|
|
from datetime import datetime, timedelta
|
|
from typing import Dict, List, Any, Optional, Callable, Set
|
|
from dataclasses import dataclass, asdict
|
|
from enum import Enum
|
|
from collections import defaultdict, deque
|
|
import threading
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
# WebSocket-Support
|
|
try:
|
|
from flask_socketio import SocketIO, emit, join_room, leave_room, disconnect
|
|
# Eventlet separat importieren für bessere Fehlerbehandlung
|
|
try:
|
|
from eventlet import wsgi, listen
|
|
import eventlet
|
|
EVENTLET_AVAILABLE = True
|
|
except (ImportError, AttributeError) as e:
|
|
# Eventlet nicht verfügbar oder nicht kompatibel (z.B. Python 3.13)
|
|
print(f"⚠️ Eventlet nicht verfügbar: {e}")
|
|
print("🔄 Fallback auf standard threading mode für WebSocket")
|
|
EVENTLET_AVAILABLE = False
|
|
eventlet = None
|
|
wsgi = None
|
|
listen = None
|
|
|
|
WEBSOCKET_AVAILABLE = True
|
|
except ImportError as e:
|
|
print(f"⚠️ Flask-SocketIO nicht verfügbar: {e}")
|
|
print("📡 Dashboard läuft ohne Real-time Updates")
|
|
WEBSOCKET_AVAILABLE = False
|
|
EVENTLET_AVAILABLE = False
|
|
SocketIO = None
|
|
eventlet = None
|
|
|
|
from flask import request, session, current_app
|
|
from flask_login import current_user
|
|
|
|
from utils.logging_config import get_logger
|
|
from models import Job, User, Printer, Stats, GuestRequest, get_db_session
|
|
from utils.analytics import track_event, get_dashboard_stats
|
|
|
|
logger = get_logger("dashboard")
|
|
|
|
class EventType(Enum):
|
|
"""Typen von Dashboard-Ereignissen"""
|
|
JOB_CREATED = "job_created"
|
|
JOB_STARTED = "job_started"
|
|
JOB_FINISHED = "job_finished"
|
|
JOB_CANCELLED = "job_cancelled"
|
|
JOB_PAUSED = "job_paused"
|
|
JOB_RESUMED = "job_resumed"
|
|
|
|
PRINTER_ONLINE = "printer_online"
|
|
PRINTER_OFFLINE = "printer_offline"
|
|
PRINTER_BUSY = "printer_busy"
|
|
PRINTER_IDLE = "printer_idle"
|
|
|
|
USER_LOGIN = "user_login"
|
|
USER_LOGOUT = "user_logout"
|
|
USER_CREATED = "user_created"
|
|
|
|
GUEST_REQUEST = "guest_request"
|
|
GUEST_APPROVED = "guest_approved"
|
|
GUEST_REJECTED = "guest_rejected"
|
|
|
|
SYSTEM_ALERT = "system_alert"
|
|
SYSTEM_WARNING = "system_warning"
|
|
SYSTEM_INFO = "system_info"
|
|
|
|
@dataclass
|
|
class DashboardEvent:
|
|
"""Dashboard-Ereignis mit Metadaten"""
|
|
event_type: EventType
|
|
data: Dict[str, Any]
|
|
timestamp: datetime
|
|
user_id: Optional[int] = None
|
|
priority: str = "normal" # low, normal, high, critical
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""Konvertiert das Ereignis zu einem Dictionary"""
|
|
return {
|
|
'event_type': self.event_type.value,
|
|
'data': self.data,
|
|
'timestamp': self.timestamp.isoformat(),
|
|
'user_id': self.user_id,
|
|
'priority': self.priority
|
|
}
|
|
|
|
@dataclass
|
|
class WidgetConfig:
|
|
"""Konfiguration für Dashboard-Widgets"""
|
|
widget_id: str
|
|
title: str
|
|
widget_type: str # chart, table, metric, alert, custom
|
|
refresh_interval: int = 30 # Sekunden
|
|
data_source: str = ""
|
|
chart_type: Optional[str] = None # line, bar, pie, donut
|
|
filters: Dict[str, Any] = None
|
|
permissions: List[str] = None
|
|
size: str = "medium" # small, medium, large, full
|
|
|
|
class DashboardManager:
|
|
"""Zentraler Manager für Dashboard-Funktionalität"""
|
|
|
|
def __init__(self):
|
|
self.socketio: Optional[SocketIO] = None
|
|
self.connected_clients: Set[str] = set()
|
|
self.user_rooms: Dict[int, Set[str]] = defaultdict(set)
|
|
self.event_queue: deque = deque(maxlen=1000)
|
|
self.event_handlers: Dict[EventType, List[Callable]] = defaultdict(list)
|
|
self.widgets: Dict[str, WidgetConfig] = {}
|
|
self.cached_data: Dict[str, Any] = {}
|
|
self.cache_timestamps: Dict[str, datetime] = {}
|
|
self.executor = ThreadPoolExecutor(max_workers=4)
|
|
|
|
self._setup_default_widgets()
|
|
self._start_background_tasks()
|
|
|
|
def init_socketio(self, app, cors_allowed_origins="*"):
|
|
"""Initialisiert SocketIO für das Dashboard"""
|
|
if not WEBSOCKET_AVAILABLE:
|
|
logger.warning("WebSocket-Funktionalität nicht verfügbar. Flask-SocketIO installieren.")
|
|
return None
|
|
|
|
# Bestimme den async_mode basierend auf verfügbaren Bibliotheken
|
|
if EVENTLET_AVAILABLE:
|
|
async_mode = 'eventlet'
|
|
logger.info("Dashboard WebSocket-Server wird mit eventlet initialisiert")
|
|
else:
|
|
async_mode = 'threading'
|
|
logger.info("Dashboard WebSocket-Server wird mit threading initialisiert (eventlet-Fallback)")
|
|
|
|
self.socketio = SocketIO(
|
|
app,
|
|
cors_allowed_origins=cors_allowed_origins,
|
|
logger=False,
|
|
engineio_logger=False,
|
|
async_mode=async_mode
|
|
)
|
|
|
|
self._setup_socket_handlers()
|
|
logger.info(f"Dashboard WebSocket-Server initialisiert (async_mode: {async_mode})")
|
|
return self.socketio
|
|
|
|
def _setup_socket_handlers(self):
|
|
"""Richtet WebSocket-Event-Handler ein"""
|
|
if not self.socketio:
|
|
return
|
|
|
|
@self.socketio.on('connect')
|
|
def handle_connect(auth):
|
|
"""Behandelt neue WebSocket-Verbindungen"""
|
|
if not current_user.is_authenticated:
|
|
logger.warning(f"Nicht authentifizierte WebSocket-Verbindung von {request.remote_addr}")
|
|
disconnect()
|
|
return False
|
|
|
|
client_id = request.sid
|
|
user_id = current_user.id
|
|
|
|
self.connected_clients.add(client_id)
|
|
self.user_rooms[user_id].add(client_id)
|
|
|
|
# Benutzer zu seiner persönlichen Room hinzufügen
|
|
join_room(f"user_{user_id}")
|
|
|
|
# Admin-Benutzer zu Admin-Room hinzufügen
|
|
if current_user.is_admin:
|
|
join_room("admin")
|
|
|
|
logger.info(f"Dashboard-Verbindung: User {user_id} ({client_id})")
|
|
|
|
# Initiale Dashboard-Daten senden
|
|
self._send_initial_data(client_id, user_id)
|
|
|
|
return True
|
|
|
|
@self.socketio.on('disconnect')
|
|
def handle_disconnect():
|
|
"""Behandelt getrennte WebSocket-Verbindungen"""
|
|
client_id = request.sid
|
|
user_id = current_user.id if current_user.is_authenticated else None
|
|
|
|
self.connected_clients.discard(client_id)
|
|
|
|
if user_id:
|
|
self.user_rooms[user_id].discard(client_id)
|
|
if not self.user_rooms[user_id]:
|
|
del self.user_rooms[user_id]
|
|
|
|
logger.info(f"Dashboard-Trennung: User {user_id} ({client_id})")
|
|
|
|
@self.socketio.on('subscribe_widget')
|
|
def handle_widget_subscription(data):
|
|
"""Behandelt Widget-Abonnements"""
|
|
widget_id = data.get('widget_id')
|
|
refresh_interval = data.get('refresh_interval', 30)
|
|
|
|
if widget_id in self.widgets:
|
|
join_room(f"widget_{widget_id}")
|
|
# Sofortige Datenaktualisierung
|
|
widget_data = self._get_widget_data(widget_id)
|
|
emit('widget_update', {
|
|
'widget_id': widget_id,
|
|
'data': widget_data,
|
|
'timestamp': datetime.now().isoformat()
|
|
})
|
|
|
|
@self.socketio.on('unsubscribe_widget')
|
|
def handle_widget_unsubscription(data):
|
|
"""Behandelt Widget-Abbestellungen"""
|
|
widget_id = data.get('widget_id')
|
|
leave_room(f"widget_{widget_id}")
|
|
|
|
@self.socketio.on('request_data')
|
|
def handle_data_request(data):
|
|
"""Behandelt manuelle Datenabfragen"""
|
|
data_type = data.get('type')
|
|
filters = data.get('filters', {})
|
|
|
|
try:
|
|
if data_type == 'jobs':
|
|
result = self._get_jobs_data(filters)
|
|
elif data_type == 'printers':
|
|
result = self._get_printers_data(filters)
|
|
elif data_type == 'users':
|
|
result = self._get_users_data(filters)
|
|
elif data_type == 'stats':
|
|
result = self._get_stats_data(filters)
|
|
else:
|
|
result = {'error': 'Unbekannter Datentyp'}
|
|
|
|
emit('data_response', {
|
|
'type': data_type,
|
|
'data': result,
|
|
'timestamp': datetime.now().isoformat()
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler bei Datenabfrage {data_type}: {str(e)}")
|
|
emit('data_response', {
|
|
'type': data_type,
|
|
'error': str(e),
|
|
'timestamp': datetime.now().isoformat()
|
|
})
|
|
|
|
def _send_initial_data(self, client_id: str, user_id: int):
|
|
"""Sendet initiale Dashboard-Daten an einen neuen Client"""
|
|
try:
|
|
# Basis-Dashboard-Daten
|
|
dashboard_data = {
|
|
'widgets': {wid: asdict(widget) for wid, widget in self.widgets.items()},
|
|
'user_permissions': self._get_user_permissions(user_id),
|
|
'system_status': self._get_system_status(),
|
|
'recent_events': [event.to_dict() for event in list(self.event_queue)[-10:]]
|
|
}
|
|
|
|
self.socketio.emit('dashboard_init', dashboard_data, room=client_id)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Senden der initialen Daten: {str(e)}")
|
|
|
|
def _setup_default_widgets(self):
|
|
"""Richtet Standard-Dashboard-Widgets ein"""
|
|
self.widgets = {
|
|
'active_jobs': WidgetConfig(
|
|
widget_id='active_jobs',
|
|
title='Aktive Jobs',
|
|
widget_type='metric',
|
|
refresh_interval=15,
|
|
data_source='jobs',
|
|
size='small'
|
|
),
|
|
'online_printers': WidgetConfig(
|
|
widget_id='online_printers',
|
|
title='Online Drucker',
|
|
widget_type='metric',
|
|
refresh_interval=30,
|
|
data_source='printers',
|
|
size='small'
|
|
),
|
|
'jobs_timeline': WidgetConfig(
|
|
widget_id='jobs_timeline',
|
|
title='Jobs Timeline',
|
|
widget_type='chart',
|
|
chart_type='line',
|
|
refresh_interval=60,
|
|
data_source='jobs',
|
|
size='large'
|
|
),
|
|
'printer_status': WidgetConfig(
|
|
widget_id='printer_status',
|
|
title='Drucker Status',
|
|
widget_type='chart',
|
|
chart_type='donut',
|
|
refresh_interval=30,
|
|
data_source='printers',
|
|
size='medium'
|
|
),
|
|
'recent_jobs': WidgetConfig(
|
|
widget_id='recent_jobs',
|
|
title='Letzte Jobs',
|
|
widget_type='table',
|
|
refresh_interval=30,
|
|
data_source='jobs',
|
|
size='large'
|
|
),
|
|
'system_alerts': WidgetConfig(
|
|
widget_id='system_alerts',
|
|
title='System-Meldungen',
|
|
widget_type='alert',
|
|
refresh_interval=10,
|
|
data_source='alerts',
|
|
size='full'
|
|
),
|
|
'user_activity': WidgetConfig(
|
|
widget_id='user_activity',
|
|
title='Benutzer-Aktivität',
|
|
widget_type='chart',
|
|
chart_type='bar',
|
|
refresh_interval=300,
|
|
data_source='users',
|
|
size='medium',
|
|
permissions=['admin']
|
|
),
|
|
'guest_requests': WidgetConfig(
|
|
widget_id='guest_requests',
|
|
title='Gast-Anfragen',
|
|
widget_type='metric',
|
|
refresh_interval=60,
|
|
data_source='guest_requests',
|
|
size='small',
|
|
permissions=['admin']
|
|
)
|
|
}
|
|
|
|
def _start_background_tasks(self):
|
|
"""Startet Hintergrundaufgaben für das Dashboard"""
|
|
def background_worker():
|
|
"""Hintergrund-Worker für regelmäßige Updates"""
|
|
while True:
|
|
try:
|
|
self._update_cached_data()
|
|
self._broadcast_widget_updates()
|
|
self._cleanup_old_events()
|
|
time.sleep(10) # Alle 10 Sekunden prüfen
|
|
except Exception as e:
|
|
logger.error(f"Fehler im Dashboard-Background-Worker: {str(e)}")
|
|
time.sleep(30) # Längere Pause bei Fehlern
|
|
|
|
# Background-Thread starten
|
|
background_thread = threading.Thread(target=background_worker, daemon=True)
|
|
background_thread.start()
|
|
logger.info("Dashboard-Background-Worker gestartet")
|
|
|
|
def _update_cached_data(self):
|
|
"""Aktualisiert gecachte Dashboard-Daten"""
|
|
current_time = datetime.now()
|
|
|
|
for widget_id, widget in self.widgets.items():
|
|
cache_key = f"widget_{widget_id}"
|
|
last_update = self.cache_timestamps.get(cache_key, datetime.min)
|
|
|
|
if (current_time - last_update).total_seconds() >= widget.refresh_interval:
|
|
try:
|
|
new_data = self._get_widget_data(widget_id)
|
|
self.cached_data[cache_key] = new_data
|
|
self.cache_timestamps[cache_key] = current_time
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Cache-Update für Widget {widget_id}: {str(e)}")
|
|
|
|
def _broadcast_widget_updates(self):
|
|
"""Sendet Widget-Updates an alle verbundenen Clients"""
|
|
if not self.socketio:
|
|
return
|
|
|
|
current_time = datetime.now()
|
|
|
|
for widget_id, widget in self.widgets.items():
|
|
cache_key = f"widget_{widget_id}"
|
|
|
|
if cache_key in self.cached_data:
|
|
widget_data = self.cached_data[cache_key]
|
|
|
|
self.socketio.emit('widget_update', {
|
|
'widget_id': widget_id,
|
|
'data': widget_data,
|
|
'timestamp': current_time.isoformat()
|
|
}, room=f"widget_{widget_id}")
|
|
|
|
def _get_widget_data(self, widget_id: str) -> Dict[str, Any]:
|
|
"""Holt Daten für ein spezifisches Widget"""
|
|
widget = self.widgets.get(widget_id)
|
|
if not widget:
|
|
return {'error': 'Widget nicht gefunden'}
|
|
|
|
try:
|
|
if widget.data_source == 'jobs':
|
|
return self._get_jobs_widget_data(widget)
|
|
elif widget.data_source == 'printers':
|
|
return self._get_printers_widget_data(widget)
|
|
elif widget.data_source == 'users':
|
|
return self._get_users_widget_data(widget)
|
|
elif widget.data_source == 'alerts':
|
|
return self._get_alerts_widget_data(widget)
|
|
elif widget.data_source == 'guest_requests':
|
|
return self._get_guest_requests_widget_data(widget)
|
|
else:
|
|
return {'error': 'Unbekannte Datenquelle'}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Laden der Widget-Daten für {widget_id}: {str(e)}")
|
|
return {'error': str(e)}
|
|
|
|
def _get_jobs_widget_data(self, widget: WidgetConfig) -> Dict[str, Any]:
|
|
"""Holt Job-Daten für Widgets"""
|
|
with get_db_session() as db_session:
|
|
if widget.widget_type == 'metric' and widget.widget_id == 'active_jobs':
|
|
# Anzahl aktiver Jobs
|
|
count = db_session.query(Job).filter(
|
|
Job.status.in_(['running', 'scheduled', 'paused'])
|
|
).count()
|
|
return {
|
|
'value': count,
|
|
'label': 'Aktive Jobs',
|
|
'trend': self._calculate_trend('active_jobs', count)
|
|
}
|
|
|
|
elif widget.widget_type == 'chart' and widget.widget_id == 'jobs_timeline':
|
|
# Jobs-Timeline für die letzten 24 Stunden
|
|
since = datetime.now() - timedelta(hours=24)
|
|
jobs = db_session.query(Job).filter(Job.created_at >= since).all()
|
|
|
|
# Gruppiere nach Stunden
|
|
hourly_data = defaultdict(int)
|
|
for job in jobs:
|
|
hour = job.created_at.replace(minute=0, second=0, microsecond=0)
|
|
hourly_data[hour] += 1
|
|
|
|
# Sortierte Timeline erstellen
|
|
timeline_data = []
|
|
for i in range(24):
|
|
hour = (datetime.now() - timedelta(hours=23-i)).replace(minute=0, second=0, microsecond=0)
|
|
timeline_data.append({
|
|
'time': hour.isoformat(),
|
|
'value': hourly_data.get(hour, 0)
|
|
})
|
|
|
|
return {
|
|
'chart_type': 'line',
|
|
'data': timeline_data,
|
|
'title': 'Jobs der letzten 24 Stunden'
|
|
}
|
|
|
|
elif widget.widget_type == 'table' and widget.widget_id == 'recent_jobs':
|
|
# Letzte 10 Jobs
|
|
jobs = db_session.query(Job).order_by(Job.created_at.desc()).limit(10).all()
|
|
|
|
job_data = []
|
|
for job in jobs:
|
|
job_data.append({
|
|
'id': job.id,
|
|
'name': job.name,
|
|
'user': job.user.name if job.user else 'Unbekannt',
|
|
'printer': job.printer.name if job.printer else 'Unbekannt',
|
|
'status': job.status,
|
|
'created': job.created_at.strftime('%d.%m.%Y %H:%M') if job.created_at else ''
|
|
})
|
|
|
|
return {
|
|
'headers': ['ID', 'Name', 'Benutzer', 'Drucker', 'Status', 'Erstellt'],
|
|
'rows': job_data
|
|
}
|
|
|
|
return {}
|
|
|
|
def _get_printers_widget_data(self, widget: WidgetConfig) -> Dict[str, Any]:
|
|
"""Holt Drucker-Daten für Widgets"""
|
|
with get_db_session() as db_session:
|
|
if widget.widget_type == 'metric' and widget.widget_id == 'online_printers':
|
|
# Anzahl online Drucker
|
|
count = db_session.query(Printer).filter(
|
|
Printer.status.in_(['online', 'idle', 'available'])
|
|
).count()
|
|
total = db_session.query(Printer).filter(Printer.active == True).count()
|
|
|
|
return {
|
|
'value': count,
|
|
'total': total,
|
|
'label': f'{count}/{total} Online',
|
|
'percentage': (count / total * 100) if total > 0 else 0
|
|
}
|
|
|
|
elif widget.widget_type == 'chart' and widget.widget_id == 'printer_status':
|
|
# Drucker-Status-Verteilung
|
|
printers = db_session.query(Printer).filter(Printer.active == True).all()
|
|
|
|
status_counts = defaultdict(int)
|
|
for printer in printers:
|
|
status_counts[printer.status] += 1
|
|
|
|
chart_data = []
|
|
colors = {
|
|
'online': '#10b981',
|
|
'idle': '#3b82f6',
|
|
'busy': '#f59e0b',
|
|
'offline': '#ef4444',
|
|
'maintenance': '#8b5cf6'
|
|
}
|
|
|
|
for status, count in status_counts.items():
|
|
chart_data.append({
|
|
'label': status.title(),
|
|
'value': count,
|
|
'color': colors.get(status, '#6b7280')
|
|
})
|
|
|
|
return {
|
|
'chart_type': 'donut',
|
|
'data': chart_data,
|
|
'title': 'Drucker Status'
|
|
}
|
|
|
|
return {}
|
|
|
|
def _get_users_widget_data(self, widget: WidgetConfig) -> Dict[str, Any]:
|
|
"""Holt Benutzer-Daten für Widgets (nur für Admins)"""
|
|
with get_db_session() as db_session:
|
|
if widget.widget_type == 'chart' and widget.widget_id == 'user_activity':
|
|
# Benutzer-Aktivität der letzten 7 Tage
|
|
since = datetime.now() - timedelta(days=7)
|
|
users = db_session.query(User).filter(
|
|
User.last_activity >= since
|
|
).all()
|
|
|
|
# Gruppiere nach Tagen
|
|
daily_data = defaultdict(set)
|
|
for user in users:
|
|
if user.last_activity:
|
|
day = user.last_activity.date()
|
|
daily_data[day].add(user.id)
|
|
|
|
# Chart-Daten erstellen
|
|
chart_data = []
|
|
for i in range(7):
|
|
day = (datetime.now() - timedelta(days=6-i)).date()
|
|
chart_data.append({
|
|
'date': day.isoformat(),
|
|
'active_users': len(daily_data.get(day, set()))
|
|
})
|
|
|
|
return {
|
|
'chart_type': 'bar',
|
|
'data': chart_data,
|
|
'title': 'Aktive Benutzer (7 Tage)'
|
|
}
|
|
|
|
return {}
|
|
|
|
def _get_guest_requests_widget_data(self, widget: WidgetConfig) -> Dict[str, Any]:
|
|
"""Holt Gast-Anfragen-Daten für Widgets"""
|
|
with get_db_session() as db_session:
|
|
if widget.widget_type == 'metric' and widget.widget_id == 'guest_requests':
|
|
# Anzahl offener Gast-Anfragen
|
|
count = db_session.query(GuestRequest).filter(
|
|
GuestRequest.status == 'pending'
|
|
).count()
|
|
|
|
return {
|
|
'value': count,
|
|
'label': 'Offene Anfragen',
|
|
'urgency': 'high' if count > 5 else 'normal'
|
|
}
|
|
|
|
return {}
|
|
|
|
def _get_alerts_widget_data(self, widget: WidgetConfig) -> Dict[str, Any]:
|
|
"""Holt System-Meldungen für Widgets"""
|
|
# Sammle aktuelle System-Alerts
|
|
alerts = []
|
|
|
|
# Prüfe auf kritische Zustände
|
|
with get_db_session() as db_session:
|
|
# Offline Drucker
|
|
offline_printers = db_session.query(Printer).filter(
|
|
Printer.status == 'offline',
|
|
Printer.active == True
|
|
).count()
|
|
|
|
if offline_printers > 0:
|
|
alerts.append({
|
|
'type': 'warning',
|
|
'message': f'{offline_printers} Drucker offline',
|
|
'timestamp': datetime.now().isoformat(),
|
|
'action': 'printers'
|
|
})
|
|
|
|
# Lange wartende Jobs
|
|
waiting_jobs = db_session.query(Job).filter(
|
|
Job.status == 'scheduled',
|
|
Job.created_at < datetime.now() - timedelta(hours=1)
|
|
).count()
|
|
|
|
if waiting_jobs > 0:
|
|
alerts.append({
|
|
'type': 'info',
|
|
'message': f'{waiting_jobs} Jobs warten seit über 1 Stunde',
|
|
'timestamp': datetime.now().isoformat(),
|
|
'action': 'jobs'
|
|
})
|
|
|
|
# Gast-Anfragen
|
|
pending_requests = db_session.query(GuestRequest).filter(
|
|
GuestRequest.status == 'pending'
|
|
).count()
|
|
|
|
if pending_requests > 3:
|
|
alerts.append({
|
|
'type': 'warning',
|
|
'message': f'{pending_requests} unbearbeitete Gast-Anfragen',
|
|
'timestamp': datetime.now().isoformat(),
|
|
'action': 'guest_requests'
|
|
})
|
|
|
|
return {
|
|
'alerts': alerts[-10:], # Nur die neuesten 10
|
|
'count': len(alerts)
|
|
}
|
|
|
|
def _calculate_trend(self, metric_key: str, current_value: int) -> str:
|
|
"""Berechnet den Trend für eine Metrik"""
|
|
# Vereinfachte Trend-Berechnung
|
|
# In einer echten Implementierung würde man historische Daten verwenden
|
|
cache_key = f"trend_{metric_key}"
|
|
previous_value = self.cached_data.get(cache_key, current_value)
|
|
|
|
if current_value > previous_value:
|
|
trend = "up"
|
|
elif current_value < previous_value:
|
|
trend = "down"
|
|
else:
|
|
trend = "stable"
|
|
|
|
self.cached_data[cache_key] = current_value
|
|
return trend
|
|
|
|
def _get_user_permissions(self, user_id: int) -> List[str]:
|
|
"""Holt Benutzer-Berechtigungen für Dashboard-Features"""
|
|
with get_db_session() as db_session:
|
|
user = db_session.query(User).filter(User.id == user_id).first()
|
|
|
|
if not user:
|
|
return []
|
|
|
|
permissions = ['view_dashboard']
|
|
|
|
if user.is_admin:
|
|
permissions.extend([
|
|
'view_all_jobs',
|
|
'manage_users',
|
|
'manage_printers',
|
|
'view_system_stats',
|
|
'view_guest_requests'
|
|
])
|
|
else:
|
|
permissions.extend([
|
|
'view_own_jobs',
|
|
'create_jobs'
|
|
])
|
|
|
|
return permissions
|
|
|
|
def _get_system_status(self) -> Dict[str, Any]:
|
|
"""Holt aktuellen System-Status"""
|
|
with get_db_session() as db_session:
|
|
# Basis-Statistiken
|
|
total_jobs = db_session.query(Job).count()
|
|
active_jobs = db_session.query(Job).filter(
|
|
Job.status.in_(['running', 'scheduled', 'paused'])
|
|
).count()
|
|
total_printers = db_session.query(Printer).filter(Printer.active == True).count()
|
|
online_printers = db_session.query(Printer).filter(
|
|
Printer.status.in_(['online', 'idle', 'available'])
|
|
).count()
|
|
total_users = db_session.query(User).filter(User.active == True).count()
|
|
|
|
return {
|
|
'jobs': {
|
|
'total': total_jobs,
|
|
'active': active_jobs
|
|
},
|
|
'printers': {
|
|
'total': total_printers,
|
|
'online': online_printers
|
|
},
|
|
'users': {
|
|
'total': total_users
|
|
},
|
|
'last_updated': datetime.now().isoformat()
|
|
}
|
|
|
|
def _cleanup_old_events(self):
|
|
"""Bereinigt alte Ereignisse aus der Queue"""
|
|
cutoff_time = datetime.now() - timedelta(hours=24)
|
|
|
|
# Entferne Ereignisse älter als 24 Stunden
|
|
while self.event_queue and self.event_queue[0].timestamp < cutoff_time:
|
|
self.event_queue.popleft()
|
|
|
|
def emit_event(self, event: DashboardEvent):
|
|
"""Sendet ein Ereignis an alle verbundenen Clients"""
|
|
if not self.socketio:
|
|
return
|
|
|
|
# Ereignis zur Queue hinzufügen
|
|
self.event_queue.append(event)
|
|
|
|
# Ereignis an relevante Clients senden
|
|
room = None
|
|
if event.user_id:
|
|
room = f"user_{event.user_id}"
|
|
elif event.priority in ['high', 'critical']:
|
|
room = "admin" # Kritische Ereignisse nur an Admins
|
|
|
|
self.socketio.emit('dashboard_event', event.to_dict(), room=room)
|
|
|
|
# Event-Handler ausführen
|
|
for handler in self.event_handlers[event.event_type]:
|
|
try:
|
|
handler(event)
|
|
except Exception as e:
|
|
logger.error(f"Fehler im Event-Handler: {str(e)}")
|
|
|
|
def register_event_handler(self, event_type: EventType, handler: Callable):
|
|
"""Registriert einen Event-Handler"""
|
|
self.event_handlers[event_type].append(handler)
|
|
|
|
def add_custom_widget(self, widget: WidgetConfig):
|
|
"""Fügt ein benutzerdefiniertes Widget hinzu"""
|
|
self.widgets[widget.widget_id] = widget
|
|
logger.info(f"Custom Widget hinzugefügt: {widget.widget_id}")
|
|
|
|
def get_dashboard_config(self, user_id: int) -> Dict[str, Any]:
|
|
"""Holt Dashboard-Konfiguration für einen Benutzer"""
|
|
user_permissions = self._get_user_permissions(user_id)
|
|
|
|
# Filtere Widgets basierend auf Berechtigungen
|
|
available_widgets = {}
|
|
for widget_id, widget in self.widgets.items():
|
|
if not widget.permissions or any(perm in user_permissions for perm in widget.permissions):
|
|
available_widgets[widget_id] = asdict(widget)
|
|
|
|
return {
|
|
'widgets': available_widgets,
|
|
'permissions': user_permissions,
|
|
'refresh_intervals': {
|
|
wid: widget.refresh_interval
|
|
for wid, widget in available_widgets.items()
|
|
}
|
|
}
|
|
|
|
# Globale Dashboard-Manager-Instanz
|
|
dashboard_manager = DashboardManager()
|
|
|
|
# Convenience-Funktionen für Events
|
|
def emit_job_event(event_type: EventType, job: Job, user_id: int = None):
|
|
"""Sendet ein Job-Ereignis"""
|
|
event = DashboardEvent(
|
|
event_type=event_type,
|
|
data={
|
|
'job_id': job.id,
|
|
'job_name': job.name,
|
|
'status': job.status,
|
|
'user_name': job.user.name if job.user else 'Unbekannt',
|
|
'printer_name': job.printer.name if job.printer else 'Unbekannt'
|
|
},
|
|
timestamp=datetime.now(),
|
|
user_id=user_id,
|
|
priority='normal'
|
|
)
|
|
dashboard_manager.emit_event(event)
|
|
|
|
def emit_printer_event(event_type: EventType, printer: Printer):
|
|
"""Sendet ein Drucker-Ereignis"""
|
|
event = DashboardEvent(
|
|
event_type=event_type,
|
|
data={
|
|
'printer_id': printer.id,
|
|
'printer_name': printer.name,
|
|
'status': printer.status,
|
|
'location': printer.location or 'Unbekannt'
|
|
},
|
|
timestamp=datetime.now(),
|
|
priority='normal' if event_type in [EventType.PRINTER_ONLINE, EventType.PRINTER_IDLE] else 'high'
|
|
)
|
|
dashboard_manager.emit_event(event)
|
|
|
|
def emit_system_alert(message: str, alert_type: str = "info", priority: str = "normal"):
|
|
"""Sendet eine System-Meldung"""
|
|
event_type_map = {
|
|
'info': EventType.SYSTEM_INFO,
|
|
'warning': EventType.SYSTEM_WARNING,
|
|
'alert': EventType.SYSTEM_ALERT
|
|
}
|
|
|
|
event = DashboardEvent(
|
|
event_type=event_type_map.get(alert_type, EventType.SYSTEM_INFO),
|
|
data={
|
|
'message': message,
|
|
'alert_type': alert_type
|
|
},
|
|
timestamp=datetime.now(),
|
|
priority=priority
|
|
)
|
|
dashboard_manager.emit_event(event)
|
|
|
|
# JavaScript für Frontend-Integration
|
|
def get_dashboard_client_js() -> str:
|
|
"""Generiert JavaScript für Dashboard-Client"""
|
|
return """
|
|
class DashboardClient {
|
|
constructor(socketUrl = '') {
|
|
this.socket = null;
|
|
this.widgets = {};
|
|
this.eventHandlers = {};
|
|
this.reconnectAttempts = 0;
|
|
this.maxReconnectAttempts = 5;
|
|
this.reconnectDelay = 1000;
|
|
|
|
this.connect(socketUrl);
|
|
}
|
|
|
|
connect(socketUrl) {
|
|
if (typeof io === 'undefined') {
|
|
console.error('Socket.IO nicht verfügbar');
|
|
return;
|
|
}
|
|
|
|
this.socket = io(socketUrl);
|
|
|
|
this.socket.on('connect', () => {
|
|
console.log('Dashboard verbunden');
|
|
this.reconnectAttempts = 0;
|
|
this.onConnected();
|
|
});
|
|
|
|
this.socket.on('disconnect', () => {
|
|
console.log('Dashboard getrennt');
|
|
this.onDisconnected();
|
|
this.attemptReconnect();
|
|
});
|
|
|
|
this.socket.on('dashboard_init', (data) => {
|
|
this.handleDashboardInit(data);
|
|
});
|
|
|
|
this.socket.on('widget_update', (data) => {
|
|
this.handleWidgetUpdate(data);
|
|
});
|
|
|
|
this.socket.on('dashboard_event', (event) => {
|
|
this.handleDashboardEvent(event);
|
|
});
|
|
|
|
this.socket.on('data_response', (data) => {
|
|
this.handleDataResponse(data);
|
|
});
|
|
}
|
|
|
|
attemptReconnect() {
|
|
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
setTimeout(() => {
|
|
console.log(`Reconnect-Versuch ${this.reconnectAttempts + 1}`);
|
|
this.reconnectAttempts++;
|
|
this.socket.connect();
|
|
}, this.reconnectDelay * Math.pow(2, this.reconnectAttempts));
|
|
}
|
|
}
|
|
|
|
subscribeWidget(widgetId, refreshInterval = 30) {
|
|
if (this.socket) {
|
|
this.socket.emit('subscribe_widget', {
|
|
widget_id: widgetId,
|
|
refresh_interval: refreshInterval
|
|
});
|
|
}
|
|
}
|
|
|
|
unsubscribeWidget(widgetId) {
|
|
if (this.socket) {
|
|
this.socket.emit('unsubscribe_widget', {
|
|
widget_id: widgetId
|
|
});
|
|
}
|
|
}
|
|
|
|
requestData(type, filters = {}) {
|
|
if (this.socket) {
|
|
this.socket.emit('request_data', {
|
|
type: type,
|
|
filters: filters
|
|
});
|
|
}
|
|
}
|
|
|
|
handleDashboardInit(data) {
|
|
this.widgets = data.widgets || {};
|
|
|
|
// Ereignis-Handler aufrufen
|
|
this.fireEvent('dashboard_init', data);
|
|
|
|
// Auto-Subscribe für alle Widgets
|
|
Object.keys(this.widgets).forEach(widgetId => {
|
|
this.subscribeWidget(widgetId, this.widgets[widgetId].refresh_interval);
|
|
});
|
|
}
|
|
|
|
handleWidgetUpdate(data) {
|
|
const widgetId = data.widget_id;
|
|
const widgetData = data.data;
|
|
|
|
// Widget-Element suchen und aktualisieren
|
|
const widgetElement = document.querySelector(`[data-widget-id="${widgetId}"]`);
|
|
if (widgetElement) {
|
|
this.updateWidgetElement(widgetElement, widgetData);
|
|
}
|
|
|
|
// Ereignis-Handler aufrufen
|
|
this.fireEvent('widget_update', data);
|
|
}
|
|
|
|
handleDashboardEvent(event) {
|
|
// Event-Notification anzeigen
|
|
this.showNotification(event);
|
|
|
|
// Ereignis-Handler aufrufen
|
|
this.fireEvent('dashboard_event', event);
|
|
}
|
|
|
|
handleDataResponse(data) {
|
|
// Ereignis-Handler aufrufen
|
|
this.fireEvent('data_response', data);
|
|
}
|
|
|
|
updateWidgetElement(element, data) {
|
|
const widgetType = element.dataset.widgetType;
|
|
|
|
switch (widgetType) {
|
|
case 'metric':
|
|
this.updateMetricWidget(element, data);
|
|
break;
|
|
case 'chart':
|
|
this.updateChartWidget(element, data);
|
|
break;
|
|
case 'table':
|
|
this.updateTableWidget(element, data);
|
|
break;
|
|
case 'alert':
|
|
this.updateAlertWidget(element, data);
|
|
break;
|
|
}
|
|
}
|
|
|
|
updateMetricWidget(element, data) {
|
|
const valueElement = element.querySelector('.metric-value');
|
|
const labelElement = element.querySelector('.metric-label');
|
|
const trendElement = element.querySelector('.metric-trend');
|
|
|
|
if (valueElement && data.value !== undefined) {
|
|
valueElement.textContent = data.value;
|
|
}
|
|
|
|
if (labelElement && data.label) {
|
|
labelElement.textContent = data.label;
|
|
}
|
|
|
|
if (trendElement && data.trend) {
|
|
trendElement.className = `metric-trend trend-${data.trend}`;
|
|
trendElement.innerHTML = this.getTrendIcon(data.trend);
|
|
}
|
|
}
|
|
|
|
updateChartWidget(element, data) {
|
|
const chartContainer = element.querySelector('.chart-container');
|
|
if (chartContainer && data.data) {
|
|
// Chart.js oder andere Chart-Library verwenden
|
|
this.renderChart(chartContainer, data);
|
|
}
|
|
}
|
|
|
|
updateTableWidget(element, data) {
|
|
const tableBody = element.querySelector('tbody');
|
|
if (tableBody && data.rows) {
|
|
tableBody.innerHTML = '';
|
|
|
|
data.rows.forEach(row => {
|
|
const tr = document.createElement('tr');
|
|
Object.values(row).forEach(cellValue => {
|
|
const td = document.createElement('td');
|
|
td.textContent = cellValue;
|
|
tr.appendChild(td);
|
|
});
|
|
tableBody.appendChild(tr);
|
|
});
|
|
}
|
|
}
|
|
|
|
updateAlertWidget(element, data) {
|
|
const alertContainer = element.querySelector('.alerts-container');
|
|
if (alertContainer && data.alerts) {
|
|
alertContainer.innerHTML = '';
|
|
|
|
data.alerts.forEach(alert => {
|
|
const alertElement = this.createAlertElement(alert);
|
|
alertContainer.appendChild(alertElement);
|
|
});
|
|
}
|
|
}
|
|
|
|
createAlertElement(alert) {
|
|
const div = document.createElement('div');
|
|
div.className = `alert alert-${alert.type}`;
|
|
div.innerHTML = `
|
|
<div class="alert-content">
|
|
<span class="alert-message">${alert.message}</span>
|
|
<span class="alert-time">${new Date(alert.timestamp).toLocaleTimeString()}</span>
|
|
</div>
|
|
`;
|
|
return div;
|
|
}
|
|
|
|
showNotification(event) {
|
|
// Toast-Notification anzeigen
|
|
const notification = document.createElement('div');
|
|
notification.className = `notification notification-${event.priority}`;
|
|
notification.innerHTML = `
|
|
<div class="notification-content">
|
|
<strong>${event.event_type.replace('_', ' ').toUpperCase()}</strong>
|
|
<p>${this.formatEventMessage(event)}</p>
|
|
</div>
|
|
<button class="notification-close">×</button>
|
|
`;
|
|
|
|
// Auto-dismiss nach 5 Sekunden
|
|
setTimeout(() => {
|
|
notification.remove();
|
|
}, 5000);
|
|
|
|
// Close-Button
|
|
notification.querySelector('.notification-close').onclick = () => {
|
|
notification.remove();
|
|
};
|
|
|
|
// Notification-Container finden oder erstellen
|
|
let container = document.querySelector('.notifications-container');
|
|
if (!container) {
|
|
container = document.createElement('div');
|
|
container.className = 'notifications-container';
|
|
document.body.appendChild(container);
|
|
}
|
|
|
|
container.appendChild(notification);
|
|
}
|
|
|
|
formatEventMessage(event) {
|
|
const data = event.data;
|
|
|
|
switch (event.event_type) {
|
|
case 'job_created':
|
|
return `Neuer Job "${data.job_name}" von ${data.user_name}`;
|
|
case 'job_started':
|
|
return `Job "${data.job_name}" gestartet auf ${data.printer_name}`;
|
|
case 'job_finished':
|
|
return `Job "${data.job_name}" abgeschlossen`;
|
|
case 'printer_offline':
|
|
return `Drucker "${data.printer_name}" ist offline`;
|
|
case 'printer_online':
|
|
return `Drucker "${data.printer_name}" ist online`;
|
|
default:
|
|
return data.message || 'Ereignis aufgetreten';
|
|
}
|
|
}
|
|
|
|
getTrendIcon(trend) {
|
|
switch (trend) {
|
|
case 'up':
|
|
return '↗';
|
|
case 'down':
|
|
return '↘';
|
|
default:
|
|
return '→';
|
|
}
|
|
}
|
|
|
|
renderChart(container, data) {
|
|
// Placeholder für Chart-Rendering
|
|
// Hier würde Chart.js oder eine andere Chart-Library verwendet
|
|
container.innerHTML = `<p>Chart: ${data.title}</p>`;
|
|
}
|
|
|
|
on(event, handler) {
|
|
if (!this.eventHandlers[event]) {
|
|
this.eventHandlers[event] = [];
|
|
}
|
|
this.eventHandlers[event].push(handler);
|
|
}
|
|
|
|
fireEvent(event, data) {
|
|
if (this.eventHandlers[event]) {
|
|
this.eventHandlers[event].forEach(handler => {
|
|
try {
|
|
handler(data);
|
|
} catch (e) {
|
|
console.error('Fehler im Event-Handler:', e);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
onConnected() {
|
|
// Connection-Status anzeigen
|
|
this.updateConnectionStatus('connected');
|
|
}
|
|
|
|
onDisconnected() {
|
|
// Connection-Status anzeigen
|
|
this.updateConnectionStatus('disconnected');
|
|
}
|
|
|
|
updateConnectionStatus(status) {
|
|
const statusElement = document.querySelector('.connection-status');
|
|
if (statusElement) {
|
|
statusElement.className = `connection-status status-${status}`;
|
|
statusElement.textContent = status === 'connected' ? 'Verbunden' : 'Getrennt';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Auto-Initialize
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
if (typeof window.dashboardClient === 'undefined') {
|
|
window.dashboardClient = new DashboardClient();
|
|
}
|
|
});
|
|
""" |