""" 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 = `
${alert.message} ${new Date(alert.timestamp).toLocaleTimeString()}
`; return div; } showNotification(event) { // Toast-Notification anzeigen const notification = document.createElement('div'); notification.className = `notification notification-${event.priority}`; notification.innerHTML = `
${event.event_type.replace('_', ' ').toUpperCase()}

${this.formatEventMessage(event)}

`; // 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 = `

Chart: ${data.title}

`; } 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(); } }); """