manage-your-printer/utils/realtime_dashboard.py
2025-06-04 10:03:22 +02:00

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">&times;</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();
}
});
"""