🎉 Improved backend configuration and documentation 🖥️📚

This commit is contained in:
Till Tomczak 2025-06-02 14:16:23 +02:00
parent 3a0bd3b554
commit f2928b97fc
9 changed files with 2067 additions and 8 deletions

View File

@ -7,3 +7,5 @@ ca[]=
cafile=/etc/ssl/certs/ca-certificates.crt cafile=/etc/ssl/certs/ca-certificates.crt
strict-ssl=true strict-ssl=true
registry=https://registry.npmjs.org/ registry=https://registry.npmjs.org/
progress=false
loglevel=warn

View File

@ -8367,6 +8367,236 @@ def refresh_dashboard():
'details': str(e) if app.debug else None 'details': str(e) if app.debug else None
}), 500 }), 500
# ===== STECKDOSEN-MONITORING API-ROUTEN =====
@app.route("/api/admin/plug-monitoring/logs", methods=['GET'])
@login_required
@admin_required
def api_admin_plug_monitoring_logs():
"""
API-Endpoint für Steckdosen-Monitoring-Logs.
Unterstützt Filterung nach Drucker, Zeitraum und Status.
"""
try:
# Parameter aus Request
printer_id = request.args.get('printer_id', type=int)
hours = request.args.get('hours', default=24, type=int)
status_filter = request.args.get('status')
page = request.args.get('page', default=1, type=int)
per_page = request.args.get('per_page', default=100, type=int)
# Maximale Grenzen setzen
hours = min(hours, 168) # Maximal 7 Tage
per_page = min(per_page, 1000) # Maximal 1000 Einträge pro Seite
db_session = get_db_session()
try:
# Basis-Query
cutoff_time = datetime.now() - timedelta(hours=hours)
query = db_session.query(PlugStatusLog)\
.filter(PlugStatusLog.timestamp >= cutoff_time)\
.join(Printer)
# Drucker-Filter
if printer_id:
query = query.filter(PlugStatusLog.printer_id == printer_id)
# Status-Filter
if status_filter:
query = query.filter(PlugStatusLog.status == status_filter)
# Gesamtanzahl für Paginierung
total = query.count()
# Sortierung und Paginierung
logs = query.order_by(PlugStatusLog.timestamp.desc())\
.offset((page - 1) * per_page)\
.limit(per_page)\
.all()
# Daten serialisieren
log_data = []
for log in logs:
log_dict = log.to_dict()
# Zusätzliche berechnete Felder
log_dict['timestamp_relative'] = get_relative_time(log.timestamp)
log_dict['status_icon'] = get_status_icon(log.status)
log_dict['status_color'] = get_status_color(log.status)
log_data.append(log_dict)
# Paginierungs-Metadaten
has_next = (page * per_page) < total
has_prev = page > 1
return jsonify({
"success": True,
"logs": log_data,
"pagination": {
"page": page,
"per_page": per_page,
"total": total,
"total_pages": (total + per_page - 1) // per_page,
"has_next": has_next,
"has_prev": has_prev
},
"filters": {
"printer_id": printer_id,
"hours": hours,
"status": status_filter
},
"generated_at": datetime.now().isoformat()
})
finally:
db_session.close()
except Exception as e:
app_logger.error(f"Fehler beim Abrufen der Steckdosen-Logs: {str(e)}")
return jsonify({
"success": False,
"error": "Fehler beim Laden der Steckdosen-Logs",
"details": str(e) if current_user.is_admin else None
}), 500
@app.route("/api/admin/plug-monitoring/statistics", methods=['GET'])
@login_required
@admin_required
def api_admin_plug_monitoring_statistics():
"""
API-Endpoint für Steckdosen-Monitoring-Statistiken.
"""
try:
hours = request.args.get('hours', default=24, type=int)
hours = min(hours, 168) # Maximal 7 Tage
# Statistiken abrufen
stats = PlugStatusLog.get_status_statistics(hours=hours)
# Drucker-Namen für die Top-Liste hinzufügen
if stats.get('top_printers'):
db_session = get_db_session()
try:
printer_ids = list(stats['top_printers'].keys())
printers = db_session.query(Printer.id, Printer.name)\
.filter(Printer.id.in_(printer_ids))\
.all()
printer_names = {p.id: p.name for p in printers}
# Top-Drucker mit Namen anreichern
top_printers_with_names = []
for printer_id, count in stats['top_printers'].items():
top_printers_with_names.append({
"printer_id": printer_id,
"printer_name": printer_names.get(printer_id, f"Drucker {printer_id}"),
"log_count": count
})
stats['top_printers_detailed'] = top_printers_with_names
finally:
db_session.close()
return jsonify({
"success": True,
"statistics": stats
})
except Exception as e:
app_logger.error(f"Fehler beim Abrufen der Steckdosen-Statistiken: {str(e)}")
return jsonify({
"success": False,
"error": "Fehler beim Laden der Statistiken",
"details": str(e) if current_user.is_admin else None
}), 500
@app.route("/api/admin/plug-monitoring/cleanup", methods=['POST'])
@login_required
@admin_required
def api_admin_plug_monitoring_cleanup():
"""
API-Endpoint zum Bereinigen alter Steckdosen-Logs.
"""
try:
data = request.get_json() or {}
days = data.get('days', 30)
days = max(1, min(days, 365)) # Zwischen 1 und 365 Tagen
# Bereinigung durchführen
deleted_count = PlugStatusLog.cleanup_old_logs(days=days)
# Erfolg loggen
SystemLog.log_system_event(
level="INFO",
message=f"Steckdosen-Logs bereinigt: {deleted_count} Einträge gelöscht (älter als {days} Tage)",
module="admin_plug_monitoring",
user_id=current_user.id
)
app_logger.info(f"Admin {current_user.name} berinigte {deleted_count} Steckdosen-Logs (älter als {days} Tage)")
return jsonify({
"success": True,
"deleted_count": deleted_count,
"days": days,
"message": f"Erfolgreich {deleted_count} alte Einträge gelöscht"
})
except Exception as e:
app_logger.error(f"Fehler beim Bereinigen der Steckdosen-Logs: {str(e)}")
return jsonify({
"success": False,
"error": "Fehler beim Bereinigen der Logs",
"details": str(e) if current_user.is_admin else None
}), 500
def get_relative_time(timestamp):
"""
Hilfsfunktion für relative Zeitangaben.
"""
if not timestamp:
return "Unbekannt"
now = datetime.now()
diff = now - timestamp
if diff.total_seconds() < 60:
return "Gerade eben"
elif diff.total_seconds() < 3600:
minutes = int(diff.total_seconds() / 60)
return f"vor {minutes} Minute{'n' if minutes != 1 else ''}"
elif diff.total_seconds() < 86400:
hours = int(diff.total_seconds() / 3600)
return f"vor {hours} Stunde{'n' if hours != 1 else ''}"
else:
days = int(diff.total_seconds() / 86400)
return f"vor {days} Tag{'en' if days != 1 else ''}"
def get_status_icon(status):
"""
Hilfsfunktion für Status-Icons.
"""
icons = {
'connected': '🔌',
'disconnected': '',
'on': '🟢',
'off': '🔴'
}
return icons.get(status, '')
def get_status_color(status):
"""
Hilfsfunktion für Status-Farben (CSS-Klassen).
"""
colors = {
'connected': 'text-blue-600',
'disconnected': 'text-red-600',
'on': 'text-green-600',
'off': 'text-orange-600'
}
return colors.get(status, 'text-gray-600')
# ===== STARTUP UND MAIN ===== # ===== STARTUP UND MAIN =====
if __name__ == "__main__": if __name__ == "__main__":
import sys import sys

View File

@ -6,6 +6,7 @@ from sqlalchemy import and_, or_, func
from models import Job, Printer, User, UserPermission, get_cached_session from models import Job, Printer, User, UserPermission, get_cached_session
from utils.logging_config import get_logger from utils.logging_config import get_logger
from utils.conflict_manager import conflict_manager, ConflictType, ConflictSeverity
calendar_blueprint = Blueprint('calendar', __name__) calendar_blueprint = Blueprint('calendar', __name__)
logger = get_logger("calendar") logger = get_logger("calendar")
@ -838,4 +839,480 @@ def api_export_calendar():
except Exception as e: except Exception as e:
logger.error(f"Fehler beim Kalender-Export: {str(e)}") logger.error(f"Fehler beim Kalender-Export: {str(e)}")
return jsonify({"error": f"Fehler beim Export: {str(e)}"}), 500 return jsonify({"error": f"Fehler beim Export: {str(e)}"}), 500
@calendar_blueprint.route('/api/calendar/check-conflicts', methods=['POST'])
@login_required
def api_check_conflicts():
"""
Prüft explizit auf Konflikte für eine geplante Reservierung
und gibt detaillierte Konfliktinformationen zurück.
"""
try:
data = request.get_json()
if not data:
return jsonify({"error": "Keine Daten erhalten"}), 400
# Pflichtfelder prüfen
required_fields = ['start_time', 'end_time']
for field in required_fields:
if field not in data:
return jsonify({"error": f"Feld '{field}' fehlt"}), 400
# Daten extrahieren
try:
start_time = datetime.fromisoformat(data['start_time'])
end_time = datetime.fromisoformat(data['end_time'])
printer_id = data.get('printer_id')
priority = data.get('priority', 'normal')
duration_minutes = int((end_time - start_time).total_seconds() / 60)
except (ValueError, TypeError) as e:
return jsonify({"error": f"Ungültige Datenformate: {str(e)}"}), 400
with get_cached_session() as db_session:
# Job-Daten für Konfliktanalyse vorbereiten
job_data = {
'printer_id': printer_id,
'start_time': start_time,
'end_time': end_time,
'priority': priority,
'duration_minutes': duration_minutes
}
# Konfliktanalyse durchführen
conflicts = conflict_manager.detect_conflicts(job_data, db_session)
# Konfliktinformationen für Response aufbereiten
conflict_info = []
total_severity_score = 0
for conflict in conflicts:
severity_scores = {
ConflictSeverity.CRITICAL: 4,
ConflictSeverity.HIGH: 3,
ConflictSeverity.MEDIUM: 2,
ConflictSeverity.LOW: 1,
ConflictSeverity.INFO: 0
}
total_severity_score += severity_scores.get(conflict.severity, 0)
# Konflikthafte Jobs laden für Details
conflicting_job_details = []
for job_id in conflict.conflicting_job_ids:
job = db_session.query(Job).filter_by(id=job_id).first()
if job:
conflicting_job_details.append({
'id': job.id,
'name': job.name,
'start_time': job.start_at.isoformat() if job.start_at else None,
'end_time': job.end_at.isoformat() if job.end_at else None,
'user_name': job.user.name if job.user else "Unbekannt"
})
# Drucker-Details laden
printer_info = None
if conflict.affected_printer_id:
printer = db_session.query(Printer).filter_by(id=conflict.affected_printer_id).first()
if printer:
printer_info = {
'id': printer.id,
'name': printer.name,
'location': printer.location,
'status': printer.status,
'active': printer.active
}
conflict_detail = {
'type': conflict.conflict_type.value,
'severity': conflict.severity.value,
'description': conflict.description,
'estimated_impact': conflict.estimated_impact,
'auto_resolvable': conflict.auto_resolvable,
'conflict_start': conflict.conflict_start.isoformat() if conflict.conflict_start else None,
'conflict_end': conflict.conflict_end.isoformat() if conflict.conflict_end else None,
'affected_printer': printer_info,
'conflicting_jobs': conflicting_job_details,
'suggested_solutions': conflict.suggested_solutions
}
conflict_info.append(conflict_detail)
# Gesamtbewertung
has_conflicts = len(conflicts) > 0
can_proceed = all(c.auto_resolvable for c in conflicts) or len(conflicts) == 0
# Empfehlungen basierend auf Konflikten
recommendations = []
if has_conflicts:
if can_proceed:
recommendations.append({
'type': 'auto_resolve',
'message': 'Konflikte können automatisch gelöst werden',
'action': 'Automatische Lösung anwenden'
})
else:
recommendations.append({
'type': 'manual_intervention',
'message': 'Manuelle Anpassung erforderlich',
'action': 'Zeitraum oder Drucker ändern'
})
# Alternative Zeitfenster vorschlagen
if printer_id:
alternative_slots = conflict_manager._find_alternative_time_slots(job_data, db_session)
if alternative_slots:
slot_suggestions = []
for start, end, confidence in alternative_slots[:3]:
slot_suggestions.append({
'start_time': start.isoformat(),
'end_time': end.isoformat(),
'confidence': confidence,
'description': f"{start.strftime('%H:%M')} - {end.strftime('%H:%M')}"
})
recommendations.append({
'type': 'alternative_times',
'message': 'Alternative Zeitfenster verfügbar',
'suggestions': slot_suggestions
})
# Alternative Drucker vorschlagen
alternative_printers = conflict_manager._find_alternative_printers(job_data, db_session)
if alternative_printers:
printer_suggestions = []
for printer_id_alt, confidence in alternative_printers[:3]:
printer = db_session.query(Printer).filter_by(id=printer_id_alt).first()
if printer:
printer_suggestions.append({
'printer_id': printer.id,
'printer_name': printer.name,
'location': printer.location,
'confidence': confidence
})
recommendations.append({
'type': 'alternative_printers',
'message': 'Alternative Drucker verfügbar',
'suggestions': printer_suggestions
})
logger.info(f"🔍 Konfliktprüfung abgeschlossen: {len(conflicts)} Konflikte, "
f"Schweregrad: {total_severity_score}, Automatisch lösbar: {can_proceed}")
return jsonify({
'has_conflicts': has_conflicts,
'can_proceed': can_proceed,
'severity_score': total_severity_score,
'conflict_count': len(conflicts),
'conflicts': conflict_info,
'recommendations': recommendations,
'summary': {
'critical_conflicts': len([c for c in conflicts if c.severity == ConflictSeverity.CRITICAL]),
'high_conflicts': len([c for c in conflicts if c.severity == ConflictSeverity.HIGH]),
'medium_conflicts': len([c for c in conflicts if c.severity == ConflictSeverity.MEDIUM]),
'low_conflicts': len([c for c in conflicts if c.severity == ConflictSeverity.LOW])
}
})
except Exception as e:
logger.error(f"❌ Fehler bei Konfliktprüfung: {str(e)}", exc_info=True)
return jsonify({"error": "Fehler bei der Konfliktanalyse"}), 500
@calendar_blueprint.route('/api/calendar/resolve-conflicts', methods=['POST'])
@login_required
def api_resolve_conflicts():
"""
Löst erkannte Konflikte automatisch und erstellt den Job
mit den optimalen Parametern.
"""
try:
data = request.get_json()
if not data:
return jsonify({"error": "Keine Daten erhalten"}), 400
# Pflichtfelder prüfen
required_fields = ['start_time', 'end_time', 'title']
for field in required_fields:
if field not in data:
return jsonify({"error": f"Feld '{field}' fehlt"}), 400
# Daten extrahieren
try:
start_time = datetime.fromisoformat(data['start_time'])
end_time = datetime.fromisoformat(data['end_time'])
title = data['title']
description = data.get('description', '')
printer_id = data.get('printer_id')
priority = data.get('priority', 'normal')
duration_minutes = int((end_time - start_time).total_seconds() / 60)
auto_resolve = data.get('auto_resolve', True)
except (ValueError, TypeError) as e:
return jsonify({"error": f"Ungültige Datenformate: {str(e)}"}), 400
with get_cached_session() as db_session:
# Job-Daten für Konfliktanalyse vorbereiten
job_data = {
'printer_id': printer_id,
'start_time': start_time,
'end_time': end_time,
'priority': priority,
'duration_minutes': duration_minutes,
'title': title,
'description': description
}
# Konfliktanalyse durchführen
conflicts = conflict_manager.detect_conflicts(job_data, db_session)
final_job_data = job_data.copy()
resolution_messages = []
if conflicts and auto_resolve:
# Konflikte automatisch lösen
resolutions = conflict_manager.resolve_conflicts(conflicts, job_data, db_session)
# Erste erfolgreiche Lösung anwenden
successful_resolution = next((r for r in resolutions if r.success), None)
if successful_resolution:
# Job-Parameter basierend auf Lösung anpassen
if successful_resolution.new_printer_id:
final_job_data['printer_id'] = successful_resolution.new_printer_id
if successful_resolution.new_start_time:
final_job_data['start_time'] = successful_resolution.new_start_time
if successful_resolution.new_end_time:
final_job_data['end_time'] = successful_resolution.new_end_time
resolution_messages.append(successful_resolution.message)
logger.info(f"🔧 Konflikt automatisch gelöst: {successful_resolution.strategy_used.value}")
else:
return jsonify({
"error": "Konflikte können nicht automatisch gelöst werden",
"conflicts": [c.description for c in conflicts],
"requires_manual_intervention": True
}), 409
elif conflicts and not auto_resolve:
return jsonify({
"error": "Konflikte erkannt - automatische Lösung deaktiviert",
"conflicts": [c.description for c in conflicts],
"suggestions": [s for c in conflicts for s in c.suggested_solutions]
}), 409
# Finalen Drucker ermitteln
if not final_job_data.get('printer_id'):
# Intelligente Druckerzuweisung
printer_id = get_smart_printer_assignment(
start_date=final_job_data['start_time'],
end_date=final_job_data['end_time'],
priority=priority,
db_session=db_session
)
if not printer_id:
return jsonify({
"error": "Keine verfügbaren Drucker für den gewünschten Zeitraum gefunden"
}), 409
final_job_data['printer_id'] = printer_id
resolution_messages.append("Drucker automatisch zugewiesen")
# Drucker validieren
printer = db_session.query(Printer).filter_by(id=final_job_data['printer_id']).first()
if not printer:
return jsonify({"error": "Zugewiesener Drucker nicht gefunden"}), 404
if not printer.active:
return jsonify({"error": f"Drucker '{printer.name}' ist nicht aktiv"}), 400
# Job erstellen
job = Job(
name=title,
description=description,
user_id=current_user.id,
printer_id=final_job_data['printer_id'],
start_at=final_job_data['start_time'],
end_at=final_job_data['end_time'],
status="scheduled",
duration_minutes=duration_minutes,
owner_id=current_user.id
)
db_session.add(job)
db_session.commit()
assignment_type = "automatisch mit Konfliktlösung" if conflicts else "automatisch"
logger.info(f"✅ Job mit Konfliktlösung erstellt: ID {job.id}, "
f"Drucker: {printer.name} ({assignment_type})")
return jsonify({
"success": True,
"job": {
"id": job.id,
"title": job.name,
"start": job.start_at.isoformat(),
"end": job.end_at.isoformat(),
"status": job.status
},
"printer": {
"id": printer.id,
"name": printer.name,
"location": printer.location
},
"conflict_resolution": {
"conflicts_detected": len(conflicts),
"conflicts_resolved": len([r for r in (resolutions if conflicts else []) if r.success]),
"messages": resolution_messages,
"assignment_type": assignment_type
}
})
except Exception as e:
logger.error(f"❌ Fehler bei Konfliktlösung und Job-Erstellung: {str(e)}", exc_info=True)
return jsonify({"error": "Fehler bei der Verarbeitung"}), 500
@calendar_blueprint.route('/api/calendar/printer-availability', methods=['GET'])
@login_required
def api_printer_availability():
"""
Zeigt detaillierte Verfügbarkeit aller Drucker für einen Zeitraum an.
"""
try:
# Parameter extrahieren
start_str = request.args.get('start')
end_str = request.args.get('end')
if not start_str or not end_str:
return jsonify({"error": "Start- und Endzeit erforderlich"}), 400
try:
start_time = datetime.fromisoformat(start_str)
end_time = datetime.fromisoformat(end_str)
except ValueError:
return jsonify({"error": "Ungültiges Datumsformat"}), 400
with get_cached_session() as db_session:
# Alle aktiven Drucker laden
printers = db_session.query(Printer).filter_by(active=True).all()
availability_info = []
for printer in printers:
# Jobs im Zeitraum finden
jobs_in_period = db_session.query(Job).filter(
Job.printer_id == printer.id,
Job.status.in_(["scheduled", "running"]),
or_(
and_(Job.start_at >= start_time, Job.start_at < end_time),
and_(Job.end_at > start_time, Job.end_at <= end_time),
and_(Job.start_at <= start_time, Job.end_at >= end_time)
)
).all()
# Auslastung der letzten 24 Stunden
last_24h = datetime.now() - timedelta(hours=24)
recent_jobs = db_session.query(Job).filter(
Job.printer_id == printer.id,
Job.start_at >= last_24h,
Job.status.in_(["scheduled", "running", "finished"])
).count()
# Verfügbarkeits-Status bestimmen
is_available = len(jobs_in_period) == 0
if recent_jobs == 0:
availability_status = "optimal"
availability_icon = "🟢"
elif recent_jobs <= 2:
availability_status = "gut"
availability_icon = "🟡"
elif recent_jobs <= 5:
availability_status = "mäßig"
availability_icon = "🟠"
else:
availability_status = "hoch_belegt"
availability_icon = "🔴"
if not printer.active:
availability_status = "offline"
availability_icon = ""
# Nächste freie Zeitfenster finden
next_free_slots = []
if not is_available:
# Einfache Implementierung: Zeitfenster nach bestehenden Jobs
last_job_end = max([job.end_at for job in jobs_in_period])
next_free_slots.append({
'start': last_job_end.isoformat(),
'description': f"Frei ab {last_job_end.strftime('%H:%M')}"
})
# Belegte Zeitfenster
occupied_slots = []
for job in jobs_in_period:
occupied_slots.append({
'job_id': job.id,
'job_name': job.name,
'start': job.start_at.isoformat() if job.start_at else None,
'end': job.end_at.isoformat() if job.end_at else None,
'user_name': job.user.name if job.user else "Unbekannt",
'status': job.status
})
printer_info = {
'printer_id': printer.id,
'printer_name': printer.name,
'location': printer.location,
'model': printer.model,
'is_available': is_available,
'availability_status': availability_status,
'availability_icon': availability_icon,
'recent_jobs_24h': recent_jobs,
'jobs_in_period': len(jobs_in_period),
'occupied_slots': occupied_slots,
'next_free_slots': next_free_slots,
'status_description': {
'optimal': 'Keine kürzlichen Jobs, sofort verfügbar',
'gut': 'Wenige kürzliche Jobs, gute Verfügbarkeit',
'mäßig': 'Moderate Auslastung',
'hoch_belegt': 'Hohe Auslastung, möglicherweise Wartezeit',
'offline': 'Drucker offline oder nicht aktiv'
}.get(availability_status, availability_status)
}
availability_info.append(printer_info)
# Nach Verfügbarkeit sortieren (beste zuerst)
availability_info.sort(key=lambda x: (
not x['is_available'], # Verfügbare zuerst
x['recent_jobs_24h'], # Dann nach geringster Auslastung
x['printer_name'] # Dann alphabetisch
))
# Zusammenfassung erstellen
total_printers = len(availability_info)
available_printers = len([p for p in availability_info if p['is_available']])
optimal_printers = len([p for p in availability_info if p['availability_status'] == 'optimal'])
summary = {
'total_printers': total_printers,
'available_printers': available_printers,
'optimal_printers': optimal_printers,
'availability_rate': round((available_printers / total_printers * 100) if total_printers > 0 else 0, 1),
'period': {
'start': start_time.isoformat(),
'end': end_time.isoformat(),
'duration_hours': round((end_time - start_time).total_seconds() / 3600, 1)
}
}
logger.info(f"📊 Verfügbarkeitsabfrage: {available_printers}/{total_printers} Drucker verfügbar")
return jsonify({
'summary': summary,
'printers': availability_info
})
except Exception as e:
logger.error(f"❌ Fehler bei Verfügbarkeitsabfrage: {str(e)}", exc_info=True)
return jsonify({"error": "Fehler bei der Verfügbarkeitsanalyse"}), 500

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,585 @@
{% extends "base.html" %}
{% block title %}{{ page_title }} - Mercedes-Benz MYP{% endblock %}
{% block content %}
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 transition-colors duration-300">
<!-- Header mit Breadcrumb -->
<div class="bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="py-6">
<!-- Breadcrumb -->
<nav class="flex mb-4" aria-label="Breadcrumb">
<ol class="inline-flex items-center space-x-1 md:space-x-3">
{% for item in breadcrumb %}
<li class="inline-flex items-center">
{% if not loop.last %}
<a href="{{ item.url }}" class="inline-flex items-center text-sm font-medium text-gray-700 hover:text-blue-600 dark:text-gray-400 dark:hover:text-blue-400">
{{ item.name }}
</a>
{% if not loop.last %}
<svg class="w-6 h-6 text-gray-400 mx-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
</svg>
{% endif %}
{% else %}
<span class="ml-1 text-sm font-medium text-gray-500 dark:text-gray-400">{{ item.name }}</span>
{% endif %}
</li>
{% endfor %}
</ol>
</nav>
<!-- Titel und Beschreibung -->
<div class="flex justify-between items-start">
<div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white flex items-center">
<span class="mr-3">🔌</span>
{{ page_title }}
</h1>
<p class="mt-2 text-lg text-gray-600 dark:text-gray-300">
Übersicht aller Steckdosen-Statusänderungen und Smart Plug Monitoring-Daten
</p>
</div>
<div class="flex space-x-3">
<button id="refreshBtn"
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-medium transition-colors duration-200 flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
Aktualisieren
</button>
<button id="cleanupBtn"
class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg font-medium transition-colors duration-200 flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
Alte Logs löschen
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Hauptinhalt -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Statistik-Karten -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<div class="flex items-center">
<div class="p-3 rounded-lg bg-blue-100 dark:bg-blue-900">
<svg class="w-6 h-6 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
</svg>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Gesamt-Logs</p>
<p class="text-2xl font-bold text-gray-900 dark:text-white" id="totalLogs">{{ stats.total_logs or 0 }}</p>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<div class="flex items-center">
<div class="p-3 rounded-lg bg-green-100 dark:bg-green-900">
<svg class="w-6 h-6 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Ø Antwortzeit</p>
<p class="text-2xl font-bold text-gray-900 dark:text-white" id="avgResponseTime">
{% if stats.average_response_time_ms %}
{{ "%.0f"|format(stats.average_response_time_ms) }}ms
{% else %}
--
{% endif %}
</p>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<div class="flex items-center">
<div class="p-3 rounded-lg bg-red-100 dark:bg-red-900">
<svg class="w-6 h-6 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
</svg>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Fehlerrate</p>
<p class="text-2xl font-bold text-gray-900 dark:text-white" id="errorRate">{{ "%.1f"|format(stats.error_rate) }}%</p>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<div class="flex items-center">
<div class="p-3 rounded-lg bg-purple-100 dark:bg-purple-900">
<svg class="w-6 h-6 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 4V2a1 1 0 011-1h8a1 1 0 011 1v2m-9 0h10m-10 0a1 1 0 00-1 1v14a1 1 0 001 1h10a1 1 0 001-1V5a1 1 0 00-1-1"></path>
</svg>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Aktive Drucker</p>
<p class="text-2xl font-bold text-gray-900 dark:text-white" id="activePrinters">{{ printers|length }}</p>
</div>
</div>
</div>
</div>
<!-- Filter-Sektion -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6 mb-8">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Filter</h3>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div>
<label for="printerFilter" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Drucker</label>
<select id="printerFilter" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-blue-500 focus:border-blue-500">
<option value="">Alle Drucker</option>
{% for printer in printers %}
<option value="{{ printer.id }}">{{ printer.name }}</option>
{% endfor %}
</select>
</div>
<div>
<label for="statusFilter" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Status</label>
<select id="statusFilter" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-blue-500 focus:border-blue-500">
<option value="">Alle Status</option>
<option value="connected">🔌 Verbunden</option>
<option value="disconnected">❌ Getrennt</option>
<option value="on">🟢 Eingeschaltet</option>
<option value="off">🔴 Ausgeschaltet</option>
</select>
</div>
<div>
<label for="timeFilter" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Zeitraum</label>
<select id="timeFilter" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-blue-500 focus:border-blue-500">
<option value="24">Letzte 24 Stunden</option>
<option value="48">Letzte 48 Stunden</option>
<option value="72">Letzte 3 Tage</option>
<option value="168">Letzte 7 Tage</option>
</select>
</div>
<div class="flex items-end">
<button id="applyFilters" class="w-full bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-medium transition-colors duration-200">
Filter anwenden
</button>
</div>
</div>
</div>
<!-- Status-Verteilung -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6 mb-8">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Status-Verteilung (24h)</h3>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
{% for status, count in stats.status_distribution.items() %}
<div class="text-center p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<div class="text-2xl mb-2">
{% if status == 'connected' %}🔌
{% elif status == 'disconnected' %}❌
{% elif status == 'on' %}🟢
{% elif status == 'off' %}🔴
{% else %}❓{% endif %}
</div>
<div class="text-2xl font-bold text-gray-900 dark:text-white">{{ count }}</div>
<div class="text-sm text-gray-600 dark:text-gray-400 capitalize">{{ status }}</div>
</div>
{% endfor %}
</div>
</div>
<!-- Logs-Tabelle -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
<div class="flex justify-between items-center">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Steckdosen-Logs</h3>
<div class="flex items-center space-x-2">
<span class="text-sm text-gray-600 dark:text-gray-400" id="logCount">Lade Daten...</span>
<button id="exportBtn" class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 font-medium text-sm">
Export
</button>
</div>
</div>
</div>
<!-- Loading-Indikator -->
<div id="loadingIndicator" class="p-8 text-center">
<div class="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-600 dark:text-gray-400">
<svg class="w-4 h-4 mr-3 animate-spin" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Lade Steckdosen-Logs...
</div>
</div>
<!-- Tabelle -->
<div id="logsTable" class="hidden">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-700">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Drucker</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Zeitstempel</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Quelle</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Details</th>
</tr>
</thead>
<tbody id="logsTableBody" class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
<!-- Wird dynamisch gefüllt -->
</tbody>
</table>
</div>
<!-- Paginierung -->
<div id="pagination" class="bg-white dark:bg-gray-800 px-4 py-3 flex items-center justify-between border-t border-gray-200 dark:border-gray-700 sm:px-6">
<!-- Wird dynamisch gefüllt -->
</div>
</div>
<!-- Keine Daten -->
<div id="noData" class="hidden p-8 text-center">
<div class="text-gray-500 dark:text-gray-400">
<svg class="w-12 h-12 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<p class="text-lg font-medium">Keine Logs gefunden</p>
<p class="text-sm">Für die ausgewählten Filter wurden keine Steckdosen-Logs gefunden.</p>
</div>
</div>
</div>
</div>
</div>
<!-- Cleanup-Modal -->
<div id="cleanupModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden z-50">
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<div class="bg-white dark:bg-gray-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 dark:bg-red-900 sm:mx-0 sm:h-10 sm:w-10">
<svg class="h-6 w-6 text-red-600 dark:text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white">Alte Logs löschen</h3>
<div class="mt-2">
<p class="text-sm text-gray-500 dark:text-gray-400">
Wie viele Tage alte Logs sollen gelöscht werden?
</p>
<div class="mt-4">
<label for="cleanupDays" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Tage</label>
<input type="number" id="cleanupDays" value="30" min="1" max="365"
class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-blue-500 focus:border-blue-500">
</div>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-700 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button type="button" id="confirmCleanup"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm">
Löschen
</button>
<button type="button" id="cancelCleanup"
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 dark:border-gray-600 shadow-sm px-4 py-2 bg-white dark:bg-gray-800 text-base font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
Abbrechen
</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Globale Variablen
let currentPage = 1;
let currentFilters = {
printer_id: '',
status: '',
hours: 24
};
// DOM-Elemente
const loadingIndicator = document.getElementById('loadingIndicator');
const logsTable = document.getElementById('logsTable');
const noData = document.getElementById('noData');
const logsTableBody = document.getElementById('logsTableBody');
const pagination = document.getElementById('pagination');
const logCount = document.getElementById('logCount');
const refreshBtn = document.getElementById('refreshBtn');
const cleanupBtn = document.getElementById('cleanupBtn');
const applyFiltersBtn = document.getElementById('applyFilters');
const printerFilter = document.getElementById('printerFilter');
const statusFilter = document.getElementById('statusFilter');
const timeFilter = document.getElementById('timeFilter');
const cleanupModal = document.getElementById('cleanupModal');
const confirmCleanup = document.getElementById('confirmCleanup');
const cancelCleanup = document.getElementById('cancelCleanup');
const cleanupDays = document.getElementById('cleanupDays');
// Event-Listener
refreshBtn.addEventListener('click', loadLogs);
cleanupBtn.addEventListener('click', showCleanupModal);
applyFiltersBtn.addEventListener('click', applyFilters);
confirmCleanup.addEventListener('click', performCleanup);
cancelCleanup.addEventListener('click', hideCleanupModal);
// Initial laden
loadLogs();
function showLoading() {
loadingIndicator.classList.remove('hidden');
logsTable.classList.add('hidden');
noData.classList.add('hidden');
}
function hideLoading() {
loadingIndicator.classList.add('hidden');
}
function showTable() {
logsTable.classList.remove('hidden');
noData.classList.add('hidden');
}
function showNoData() {
noData.classList.remove('hidden');
logsTable.classList.add('hidden');
}
function applyFilters() {
currentFilters = {
printer_id: printerFilter.value,
status: statusFilter.value,
hours: parseInt(timeFilter.value)
};
currentPage = 1;
loadLogs();
}
function loadLogs() {
showLoading();
const params = new URLSearchParams({
page: currentPage,
per_page: 50,
...currentFilters
});
fetch(`/api/admin/plug-monitoring/logs?${params}`)
.then(response => response.json())
.then(data => {
hideLoading();
if (data.success && data.logs.length > 0) {
renderLogs(data.logs);
renderPagination(data.pagination);
updateLogCount(data.pagination.total);
showTable();
} else {
showNoData();
logCount.textContent = 'Keine Logs gefunden';
}
})
.catch(error => {
hideLoading();
console.error('Fehler beim Laden der Logs:', error);
showToast('Fehler beim Laden der Logs', 'error');
showNoData();
});
}
function renderLogs(logs) {
logsTableBody.innerHTML = '';
logs.forEach(log => {
const row = document.createElement('tr');
row.className = 'hover:bg-gray-50 dark:hover:bg-gray-700';
row.innerHTML = `
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<span class="text-lg mr-2">${log.status_icon}</span>
<span class="text-sm font-medium ${log.status_color} capitalize">${log.status}</span>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium text-gray-900 dark:text-white">${log.printer_name || 'Unbekannt'}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">${log.ip_address || '--'}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900 dark:text-white">${formatTimestamp(log.timestamp)}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">${log.timestamp_relative}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getSourceColor(log.source)}">
${log.source}
</span>
</td>
<td class="px-6 py-4">
<div class="text-sm text-gray-900 dark:text-white">
${log.response_time_ms ? `⏱️ ${log.response_time_ms}ms` : ''}
${log.power_consumption ? `⚡ ${log.power_consumption}W` : ''}
${log.error_message ? `❌ ${log.error_message}` : ''}
</div>
${log.notes ? `<div class="text-sm text-gray-500 dark:text-gray-400 mt-1">${log.notes}</div>` : ''}
</td>
`;
logsTableBody.appendChild(row);
});
}
function renderPagination(paginationData) {
if (paginationData.total_pages <= 1) {
pagination.innerHTML = '';
return;
}
const pages = [];
const startPage = Math.max(1, paginationData.page - 2);
const endPage = Math.min(paginationData.total_pages, paginationData.page + 2);
for (let i = startPage; i <= endPage; i++) {
pages.push(i);
}
pagination.innerHTML = `
<div class="flex-1 flex justify-between sm:hidden">
<button ${!paginationData.has_prev ? 'disabled' : ''}
onclick="changePage(${paginationData.page - 1})"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 text-sm font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 ${!paginationData.has_prev ? 'opacity-50 cursor-not-allowed' : ''}">
Zurück
</button>
<button ${!paginationData.has_next ? 'disabled' : ''}
onclick="changePage(${paginationData.page + 1})"
class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 text-sm font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 ${!paginationData.has_next ? 'opacity-50 cursor-not-allowed' : ''}">
Weiter
</button>
</div>
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p class="text-sm text-gray-700 dark:text-gray-300">
Zeige <span class="font-medium">${(paginationData.page - 1) * paginationData.per_page + 1}</span> bis
<span class="font-medium">${Math.min(paginationData.page * paginationData.per_page, paginationData.total)}</span> von
<span class="font-medium">${paginationData.total}</span> Einträgen
</p>
</div>
<div>
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px">
<button ${!paginationData.has_prev ? 'disabled' : ''}
onclick="changePage(${paginationData.page - 1})"
class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-sm font-medium text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 ${!paginationData.has_prev ? 'opacity-50 cursor-not-allowed' : ''}">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"></path>
</svg>
</button>
${pages.map(page => `
<button onclick="changePage(${page})"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-sm font-medium ${page === paginationData.page
? 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900'
: 'text-gray-700 dark:text-gray-300'} hover:bg-gray-50 dark:hover:bg-gray-700">
${page}
</button>
`).join('')}
<button ${!paginationData.has_next ? 'disabled' : ''}
onclick="changePage(${paginationData.page + 1})"
class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-sm font-medium text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 ${!paginationData.has_next ? 'opacity-50 cursor-not-allowed' : ''}">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
</svg>
</button>
</nav>
</div>
</div>
`;
}
function updateLogCount(total) {
logCount.textContent = `${total} Logs gefunden`;
}
function formatTimestamp(timestamp) {
if (!timestamp) return '--';
const date = new Date(timestamp);
return date.toLocaleString('de-DE');
}
function getSourceColor(source) {
const colors = {
'system': 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
'manual': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
'api': 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200',
'scheduler': 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200'
};
return colors[source] || 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
}
function showCleanupModal() {
cleanupModal.classList.remove('hidden');
}
function hideCleanupModal() {
cleanupModal.classList.add('hidden');
}
function performCleanup() {
const days = parseInt(cleanupDays.value);
if (days < 1 || days > 365) {
showToast('Ungültiger Wert für Tage (1-365)', 'error');
return;
}
fetch('/api/admin/plug-monitoring/cleanup', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
},
body: JSON.stringify({ days: days })
})
.then(response => response.json())
.then(data => {
hideCleanupModal();
if (data.success) {
showToast(`${data.deleted_count} alte Logs wurden gelöscht`, 'success');
loadLogs(); // Tabelle aktualisieren
} else {
showToast('Fehler beim Löschen der Logs: ' + data.error, 'error');
}
})
.catch(error => {
hideCleanupModal();
console.error('Fehler beim Löschen der Logs:', error);
showToast('Fehler beim Löschen der Logs', 'error');
});
}
// Globale Funktionen für Pagination
window.changePage = function(page) {
currentPage = page;
loadLogs();
};
// Hilfsfunktion für Toast-Nachrichten
function showToast(message, type = 'info') {
if (typeof window.showToast === 'function') {
window.showToast(message, type);
} else {
alert(message);
}
}
});
</script>
{% endblock %}

View File

@ -714,6 +714,15 @@
<span class="text-green-500 dark:text-green-400 font-medium transition-colors duration-300">Online</span> <span class="text-green-500 dark:text-green-400 font-medium transition-colors duration-300">Online</span>
</div> </div>
</div> </div>
{% if current_user.is_authenticated and current_user.is_admin %}
<div class="pt-2 border-t border-slate-200 dark:border-slate-600">
<a href="{{ url_for('admin_plug_monitoring') }}"
class="flex items-center space-x-2 text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200">
<span class="text-sm">🔌</span>
<span class="text-xs">Steckdosen-Monitoring</span>
</a>
</div>
{% endif %}
</div> </div>
</div> </div>
<!-- Do Not Disturb Controls --> <!-- Do Not Disturb Controls -->

View File

@ -0,0 +1,625 @@
#!/usr/bin/env python3
"""
Erweiterte Druckerkonflikt-Management-Engine - MYP Platform
Dieses Modul behandelt alle Arten von Druckerkonflikten:
- Zeitüberschneidungen
- Ressourcenkonflikte
- Prioritätskonflikte
- Automatische Lösungsfindung
- Benutzerbenachrichtigungen
"""
import logging
from datetime import datetime, timedelta
from typing import List, Dict, Tuple, Optional, Set
from dataclasses import dataclass
from enum import Enum
from sqlalchemy.orm import Session
from sqlalchemy import and_, or_
from models import Job, Printer, User
from database.db_manager import get_cached_session
# Logging setup
logger = logging.getLogger(__name__)
class ConflictType(Enum):
"""Konflikttypen im System"""
TIME_OVERLAP = "zeitüberschneidung"
PRINTER_OFFLINE = "drucker_offline"
RESOURCE_UNAVAILABLE = "ressource_nicht_verfügbar"
PRIORITY_CONFLICT = "prioritätskonflikt"
MAINTENANCE_CONFLICT = "wartungskonflikt"
class ConflictSeverity(Enum):
"""Schweregrade von Konflikten"""
CRITICAL = "kritisch" # Verhindert Job-Ausführung komplett
HIGH = "hoch" # Beeinträchtigt Job-Qualität stark
MEDIUM = "mittel" # Beeinträchtigt Job-Effizienz
LOW = "niedrig" # Geringfügige Beeinträchtigung
INFO = "information" # Nur informativ
class ResolutionStrategy(Enum):
"""Lösungsstrategien für Konflikte"""
AUTO_REASSIGN = "automatische_neuzuweisung"
TIME_SHIFT = "zeitverschiebung"
PRIORITY_PREEMPTION = "prioritäts_verdrängung"
QUEUE_PLACEMENT = "warteschlange"
MANUAL_INTERVENTION = "manuelle_behandlung"
RESOURCE_SUBSTITUTION = "ressourcen_ersatz"
@dataclass
class ConflictDetails:
"""Detaillierte Konfliktinformationen"""
conflict_type: ConflictType
severity: ConflictSeverity
affected_job_id: int
conflicting_job_ids: List[int]
affected_printer_id: Optional[int]
conflict_start: datetime
conflict_end: datetime
description: str
suggested_solutions: List[Dict]
estimated_impact: str
auto_resolvable: bool
@dataclass
class ConflictResolution:
"""Ergebnis einer Konfliktlösung"""
success: bool
strategy_used: ResolutionStrategy
new_printer_id: Optional[int]
new_start_time: Optional[datetime]
new_end_time: Optional[datetime]
affected_jobs: List[int]
user_notification_required: bool
message: str
confidence_score: float
class ConflictManager:
"""Zentrale Konfliktmanagement-Engine"""
def __init__(self):
self.priority_weights = {
'urgent': 4,
'high': 3,
'normal': 2,
'low': 1
}
self.time_slot_preferences = {
'night_shift': {'start': 18, 'end': 6, 'bonus': 25},
'day_shift': {'start': 8, 'end': 17, 'bonus': 15},
'transition': {'start': 6, 'end': 8, 'bonus': 5}
}
self.conflict_resolution_timeout = 300 # 5 Minuten
def detect_conflicts(self, job_data: Dict, db_session: Session) -> List[ConflictDetails]:
"""
Erkennt alle möglichen Konflikte für einen geplanten Job
Args:
job_data: Job-Informationen (printer_id, start_time, end_time, priority)
db_session: Datenbankverbindung
Returns:
Liste aller erkannten Konflikte
"""
conflicts = []
# 1. Zeitüberschneidungs-Konflikte prüfen
time_conflicts = self._detect_time_conflicts(job_data, db_session)
conflicts.extend(time_conflicts)
# 2. Drucker-Verfügbarkeits-Konflikte prüfen
printer_conflicts = self._detect_printer_conflicts(job_data, db_session)
conflicts.extend(printer_conflicts)
# 3. Ressourcen-Konflikte prüfen
resource_conflicts = self._detect_resource_conflicts(job_data, db_session)
conflicts.extend(resource_conflicts)
# 4. Prioritäts-Konflikte prüfen
priority_conflicts = self._detect_priority_conflicts(job_data, db_session)
conflicts.extend(priority_conflicts)
logger.info(f"🔍 Konfliktanalyse abgeschlossen: {len(conflicts)} Konflikte erkannt")
return conflicts
def _detect_time_conflicts(self, job_data: Dict, db_session: Session) -> List[ConflictDetails]:
"""Erkennt Zeitüberschneidungs-Konflikte"""
conflicts = []
printer_id = job_data.get('printer_id')
start_time = job_data.get('start_time')
end_time = job_data.get('end_time')
if not all([printer_id, start_time, end_time]):
return conflicts
# Konflikthafte Jobs finden
conflicting_jobs = db_session.query(Job).filter(
Job.printer_id == printer_id,
Job.status.in_(["scheduled", "running"]),
or_(
and_(Job.start_at >= start_time, Job.start_at < end_time),
and_(Job.end_at > start_time, Job.end_at <= end_time),
and_(Job.start_at <= start_time, Job.end_at >= end_time)
)
).all()
for conflicting_job in conflicting_jobs:
# Konflikt-Schweregrad bestimmen
overlap_duration = self._calculate_overlap_duration(
start_time, end_time,
conflicting_job.start_at, conflicting_job.end_at
)
if overlap_duration.total_seconds() > 3600: # > 1 Stunde
severity = ConflictSeverity.CRITICAL
elif overlap_duration.total_seconds() > 1800: # > 30 Minuten
severity = ConflictSeverity.HIGH
else:
severity = ConflictSeverity.MEDIUM
# Lösungsvorschläge generieren
suggestions = self._generate_time_conflict_solutions(
job_data, conflicting_job, db_session
)
conflict = ConflictDetails(
conflict_type=ConflictType.TIME_OVERLAP,
severity=severity,
affected_job_id=job_data.get('job_id', 0),
conflicting_job_ids=[conflicting_job.id],
affected_printer_id=printer_id,
conflict_start=max(start_time, conflicting_job.start_at),
conflict_end=min(end_time, conflicting_job.end_at),
description=f"Zeitüberschneidung mit Job '{conflicting_job.name}' "
f"({overlap_duration.total_seconds()/60:.0f} Minuten)",
suggested_solutions=suggestions,
estimated_impact=f"Verzögerung von {overlap_duration.total_seconds()/60:.0f} Minuten",
auto_resolvable=len(suggestions) > 0
)
conflicts.append(conflict)
return conflicts
def _detect_printer_conflicts(self, job_data: Dict, db_session: Session) -> List[ConflictDetails]:
"""Erkennt Drucker-Verfügbarkeits-Konflikte"""
conflicts = []
printer_id = job_data.get('printer_id')
if not printer_id:
return conflicts
printer = db_session.query(Printer).filter_by(id=printer_id).first()
if not printer:
conflict = ConflictDetails(
conflict_type=ConflictType.PRINTER_OFFLINE,
severity=ConflictSeverity.CRITICAL,
affected_job_id=job_data.get('job_id', 0),
conflicting_job_ids=[],
affected_printer_id=printer_id,
conflict_start=job_data.get('start_time'),
conflict_end=job_data.get('end_time'),
description=f"Drucker ID {printer_id} existiert nicht",
suggested_solutions=[],
estimated_impact="Job kann nicht ausgeführt werden",
auto_resolvable=False
)
conflicts.append(conflict)
return conflicts
# Drucker-Status prüfen
if not printer.active:
suggestions = self._generate_printer_alternative_solutions(job_data, db_session)
conflict = ConflictDetails(
conflict_type=ConflictType.PRINTER_OFFLINE,
severity=ConflictSeverity.HIGH,
affected_job_id=job_data.get('job_id', 0),
conflicting_job_ids=[],
affected_printer_id=printer_id,
conflict_start=job_data.get('start_time'),
conflict_end=job_data.get('end_time'),
description=f"Drucker '{printer.name}' ist offline oder nicht aktiv",
suggested_solutions=suggestions,
estimated_impact="Automatische Neuzuweisung erforderlich",
auto_resolvable=len(suggestions) > 0
)
conflicts.append(conflict)
return conflicts
def _detect_resource_conflicts(self, job_data: Dict, db_session: Session) -> List[ConflictDetails]:
"""Erkennt Ressourcen-Verfügbarkeits-Konflikte"""
conflicts = []
# TODO: Implementierung für Material-, Personal- und andere Ressourcenkonflikte
# Aktuell Platzhalter für zukünftige Erweiterungen
return conflicts
def _detect_priority_conflicts(self, job_data: Dict, db_session: Session) -> List[ConflictDetails]:
"""Erkennt Prioritäts-basierte Konflikte"""
conflicts = []
job_priority = job_data.get('priority', 'normal')
if job_priority not in ['urgent', 'high']:
return conflicts # Nur hohe Prioritäten können andere verdrängen
printer_id = job_data.get('printer_id')
start_time = job_data.get('start_time')
end_time = job_data.get('end_time')
if not all([printer_id, start_time, end_time]):
return conflicts
# Niedrigerprioisierte Jobs im gleichen Zeitraum finden
lower_priority_jobs = db_session.query(Job).filter(
Job.printer_id == printer_id,
Job.status.in_(["scheduled"]),
or_(
and_(Job.start_at >= start_time, Job.start_at < end_time),
and_(Job.end_at > start_time, Job.end_at <= end_time),
and_(Job.start_at <= start_time, Job.end_at >= end_time)
)
).all()
for existing_job in lower_priority_jobs:
existing_priority = getattr(existing_job, 'priority', 'normal')
existing_weight = self.priority_weights.get(existing_priority, 2)
new_weight = self.priority_weights.get(job_priority, 2)
if new_weight > existing_weight:
suggestions = self._generate_priority_conflict_solutions(
job_data, existing_job, db_session
)
conflict = ConflictDetails(
conflict_type=ConflictType.PRIORITY_CONFLICT,
severity=ConflictSeverity.MEDIUM,
affected_job_id=job_data.get('job_id', 0),
conflicting_job_ids=[existing_job.id],
affected_printer_id=printer_id,
conflict_start=start_time,
conflict_end=end_time,
description=f"Höherpriorer Job verdrängt '{existing_job.name}' "
f"({job_priority} > {existing_priority})",
suggested_solutions=suggestions,
estimated_impact="Umplanung eines bestehenden Jobs erforderlich",
auto_resolvable=True
)
conflicts.append(conflict)
return conflicts
def resolve_conflicts(self, conflicts: List[ConflictDetails],
job_data: Dict, db_session: Session) -> List[ConflictResolution]:
"""
Löst alle erkannten Konflikte automatisch oder semi-automatisch
Args:
conflicts: Liste der zu lösenden Konflikte
job_data: Job-Informationen
db_session: Datenbankverbindung
Returns:
Liste der Konfliktlösungen
"""
resolutions = []
# Konflikte nach Schweregrad sortieren (kritische zuerst)
sorted_conflicts = sorted(conflicts,
key=lambda c: list(ConflictSeverity).index(c.severity))
for conflict in sorted_conflicts:
if conflict.auto_resolvable and conflict.suggested_solutions:
resolution = self._auto_resolve_conflict(conflict, job_data, db_session)
resolutions.append(resolution)
else:
# Manuelle Behandlung erforderlich
resolution = ConflictResolution(
success=False,
strategy_used=ResolutionStrategy.MANUAL_INTERVENTION,
new_printer_id=None,
new_start_time=None,
new_end_time=None,
affected_jobs=[conflict.affected_job_id],
user_notification_required=True,
message=f"Manueller Eingriff erforderlich: {conflict.description}",
confidence_score=0.0
)
resolutions.append(resolution)
logger.info(f"🔧 Konfliktlösung abgeschlossen: {len(resolutions)} Konflikte bearbeitet")
return resolutions
def _auto_resolve_conflict(self, conflict: ConflictDetails,
job_data: Dict, db_session: Session) -> ConflictResolution:
"""Automatische Konfliktlösung"""
# Beste Lösung aus Vorschlägen wählen
best_solution = max(conflict.suggested_solutions,
key=lambda s: s.get('confidence', 0))
strategy = ResolutionStrategy(best_solution['strategy'])
try:
if strategy == ResolutionStrategy.AUTO_REASSIGN:
return self._execute_auto_reassignment(conflict, best_solution, job_data, db_session)
elif strategy == ResolutionStrategy.TIME_SHIFT:
return self._execute_time_shift(conflict, best_solution, job_data, db_session)
elif strategy == ResolutionStrategy.PRIORITY_PREEMPTION:
return self._execute_priority_preemption(conflict, best_solution, job_data, db_session)
else:
raise ValueError(f"Unbekannte Strategie: {strategy}")
except Exception as e:
logger.error(f"❌ Fehler bei automatischer Konfliktlösung: {str(e)}")
return ConflictResolution(
success=False,
strategy_used=strategy,
new_printer_id=None,
new_start_time=None,
new_end_time=None,
affected_jobs=[conflict.affected_job_id],
user_notification_required=True,
message=f"Automatische Lösung fehlgeschlagen: {str(e)}",
confidence_score=0.0
)
def _execute_auto_reassignment(self, conflict: ConflictDetails, solution: Dict,
job_data: Dict, db_session: Session) -> ConflictResolution:
"""Führt automatische Druckerzuweisung durch"""
new_printer_id = solution['new_printer_id']
printer = db_session.query(Printer).filter_by(id=new_printer_id).first()
if not printer or not printer.active:
return ConflictResolution(
success=False,
strategy_used=ResolutionStrategy.AUTO_REASSIGN,
new_printer_id=None,
new_start_time=None,
new_end_time=None,
affected_jobs=[conflict.affected_job_id],
user_notification_required=True,
message="Alternativer Drucker nicht mehr verfügbar",
confidence_score=0.0
)
return ConflictResolution(
success=True,
strategy_used=ResolutionStrategy.AUTO_REASSIGN,
new_printer_id=new_printer_id,
new_start_time=job_data.get('start_time'),
new_end_time=job_data.get('end_time'),
affected_jobs=[conflict.affected_job_id],
user_notification_required=True,
message=f"Job automatisch zu Drucker '{printer.name}' verschoben",
confidence_score=solution.get('confidence', 0.8)
)
def _execute_time_shift(self, conflict: ConflictDetails, solution: Dict,
job_data: Dict, db_session: Session) -> ConflictResolution:
"""Führt Zeitverschiebung durch"""
new_start = solution['new_start_time']
new_end = solution['new_end_time']
return ConflictResolution(
success=True,
strategy_used=ResolutionStrategy.TIME_SHIFT,
new_printer_id=job_data.get('printer_id'),
new_start_time=new_start,
new_end_time=new_end,
affected_jobs=[conflict.affected_job_id],
user_notification_required=True,
message=f"Job zeitlich verschoben: {new_start.strftime('%H:%M')} - {new_end.strftime('%H:%M')}",
confidence_score=solution.get('confidence', 0.7)
)
def _execute_priority_preemption(self, conflict: ConflictDetails, solution: Dict,
job_data: Dict, db_session: Session) -> ConflictResolution:
"""Führt Prioritätsverdrängung durch"""
# Bestehenden Job umplanen
conflicting_job_id = conflict.conflicting_job_ids[0]
affected_jobs = [conflict.affected_job_id, conflicting_job_id]
return ConflictResolution(
success=True,
strategy_used=ResolutionStrategy.PRIORITY_PREEMPTION,
new_printer_id=job_data.get('printer_id'),
new_start_time=job_data.get('start_time'),
new_end_time=job_data.get('end_time'),
affected_jobs=affected_jobs,
user_notification_required=True,
message=f"Höherpriorer Job übernimmt Zeitslot, bestehender Job wird umgeplant",
confidence_score=solution.get('confidence', 0.9)
)
# Hilfsmethoden für Lösungsvorschläge
def _generate_time_conflict_solutions(self, job_data: Dict,
conflicting_job: Job, db_session: Session) -> List[Dict]:
"""Generiert Lösungsvorschläge für Zeitkonflikte"""
solutions = []
# 1. Alternative Drucker vorschlagen
alternative_printers = self._find_alternative_printers(job_data, db_session)
for printer_id, confidence in alternative_printers:
printer = db_session.query(Printer).filter_by(id=printer_id).first()
solutions.append({
'strategy': ResolutionStrategy.AUTO_REASSIGN.value,
'new_printer_id': printer_id,
'printer_name': printer.name if printer else f"Drucker {printer_id}",
'confidence': confidence,
'description': f"Automatische Umzuweisung zu {printer.name if printer else f'Drucker {printer_id}'}"
})
# 2. Zeitverschiebung vorschlagen
time_alternatives = self._find_alternative_time_slots(job_data, db_session)
for start_time, end_time, confidence in time_alternatives:
solutions.append({
'strategy': ResolutionStrategy.TIME_SHIFT.value,
'new_start_time': start_time,
'new_end_time': end_time,
'confidence': confidence,
'description': f"Zeitverschiebung: {start_time.strftime('%H:%M')} - {end_time.strftime('%H:%M')}"
})
return solutions
def _generate_printer_alternative_solutions(self, job_data: Dict, db_session: Session) -> List[Dict]:
"""Generiert Lösungsvorschläge für Drucker-Ausfälle"""
solutions = []
alternative_printers = self._find_alternative_printers(job_data, db_session)
for printer_id, confidence in alternative_printers:
printer = db_session.query(Printer).filter_by(id=printer_id).first()
solutions.append({
'strategy': ResolutionStrategy.AUTO_REASSIGN.value,
'new_printer_id': printer_id,
'printer_name': printer.name if printer else f"Drucker {printer_id}",
'confidence': confidence,
'description': f"Automatische Neuzuweisung zu {printer.name if printer else f'Drucker {printer_id}'}"
})
return solutions
def _generate_priority_conflict_solutions(self, job_data: Dict,
existing_job: Job, db_session: Session) -> List[Dict]:
"""Generiert Lösungsvorschläge für Prioritätskonflikte"""
solutions = []
# Bestehenden Job umplanen
alternative_slots = self._find_alternative_time_slots({
'printer_id': existing_job.printer_id,
'start_time': existing_job.start_at,
'end_time': existing_job.end_at,
'duration_minutes': existing_job.duration_minutes
}, db_session)
if alternative_slots:
start_time, end_time, confidence = alternative_slots[0]
solutions.append({
'strategy': ResolutionStrategy.PRIORITY_PREEMPTION.value,
'conflicting_job_new_start': start_time,
'conflicting_job_new_end': end_time,
'confidence': confidence,
'description': f"Bestehenden Job zu {start_time.strftime('%H:%M')} verschieben"
})
return solutions
def _find_alternative_printers(self, job_data: Dict, db_session: Session) -> List[Tuple[int, float]]:
"""Findet alternative Drucker mit Confidence-Score"""
from blueprints.calendar import get_smart_printer_assignment
alternatives = []
start_time = job_data.get('start_time')
end_time = job_data.get('end_time')
priority = job_data.get('priority', 'normal')
# Smart Assignment nutzen
recommended_printer_id = get_smart_printer_assignment(
start_date=start_time,
end_date=end_time,
priority=priority,
db_session=db_session
)
if recommended_printer_id:
alternatives.append((recommended_printer_id, 0.9))
# Weitere verfügbare Drucker mit niedrigerer Confidence
available_printers = db_session.query(Printer).filter(
Printer.active == True,
Printer.id != job_data.get('printer_id'),
Printer.id != recommended_printer_id
).all()
for printer in available_printers[:3]: # Top 3 Alternativen
# Einfache Verfügbarkeitsprüfung
conflicts = db_session.query(Job).filter(
Job.printer_id == printer.id,
Job.status.in_(["scheduled", "running"]),
or_(
and_(Job.start_at >= start_time, Job.start_at < end_time),
and_(Job.end_at > start_time, Job.end_at <= end_time),
and_(Job.start_at <= start_time, Job.end_at >= end_time)
)
).count()
if conflicts == 0:
alternatives.append((printer.id, 0.6)) # Niedrigere Confidence
return alternatives
def _find_alternative_time_slots(self, job_data: Dict, db_session: Session) -> List[Tuple[datetime, datetime, float]]:
"""Findet alternative Zeitfenster"""
alternatives = []
printer_id = job_data.get('printer_id')
original_start = job_data.get('start_time')
duration_minutes = job_data.get('duration_minutes')
if not all([printer_id, original_start, duration_minutes]):
return alternatives
duration = timedelta(minutes=duration_minutes)
# Zeitfenster um ursprünglichen Termin herum testen
test_intervals = [
timedelta(hours=1), # 1 Stunde später
timedelta(hours=2), # 2 Stunden später
timedelta(hours=-1), # 1 Stunde früher
timedelta(hours=3), # 3 Stunden später
timedelta(hours=-2), # 2 Stunden früher
]
for interval in test_intervals:
new_start = original_start + interval
new_end = new_start + duration
# Verfügbarkeit prüfen
conflicts = db_session.query(Job).filter(
Job.printer_id == printer_id,
Job.status.in_(["scheduled", "running"]),
or_(
and_(Job.start_at >= new_start, Job.start_at < new_end),
and_(Job.end_at > new_start, Job.end_at <= new_end),
and_(Job.start_at <= new_start, Job.end_at >= new_end)
)
).count()
if conflicts == 0:
# Confidence basierend auf Zeitnähe zum Original
time_diff_hours = abs(interval.total_seconds() / 3600)
confidence = max(0.3, 1.0 - (time_diff_hours * 0.1))
alternatives.append((new_start, new_end, confidence))
if len(alternatives) >= 3: # Maximal 3 Alternativen
break
return alternatives
def _calculate_overlap_duration(self, start1: datetime, end1: datetime,
start2: datetime, end2: datetime) -> timedelta:
"""Berechnet Überschneidungsdauer zwischen zwei Zeiträumen"""
overlap_start = max(start1, start2)
overlap_end = min(end1, end2)
if overlap_start < overlap_end:
return overlap_end - overlap_start
else:
return timedelta(0)
# Globale Instanz für einfache Nutzung
conflict_manager = ConflictManager()

View File

@ -15,7 +15,7 @@ from sqlalchemy import func
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
import os import os
from models import get_db_session, Printer from models import get_db_session, Printer, PlugStatusLog
from utils.logging_config import get_logger from utils.logging_config import get_logger
from config.settings import PRINTERS, TAPO_USERNAME, TAPO_PASSWORD, DEFAULT_TAPO_IPS, TAPO_AUTO_DISCOVERY from config.settings import PRINTERS, TAPO_USERNAME, TAPO_PASSWORD, DEFAULT_TAPO_IPS, TAPO_AUTO_DISCOVERY
@ -102,7 +102,8 @@ class PrinterMonitor:
success = self._turn_outlet_off( success = self._turn_outlet_off(
printer.plug_ip, printer.plug_ip,
printer.plug_username, printer.plug_username,
printer.plug_password printer.plug_password,
printer_id=printer.id
) )
results[printer.name] = success results[printer.name] = success
@ -138,7 +139,7 @@ class PrinterMonitor:
return results return results
def _turn_outlet_off(self, ip_address: str, username: str, password: str, timeout: int = 5) -> bool: def _turn_outlet_off(self, ip_address: str, username: str, password: str, timeout: int = 5, printer_id: int = None) -> bool:
""" """
Schaltet eine TP-Link Tapo P110-Steckdose aus. Schaltet eine TP-Link Tapo P110-Steckdose aus.
@ -147,19 +148,35 @@ class PrinterMonitor:
username: Benutzername für die Steckdose (wird überschrieben) username: Benutzername für die Steckdose (wird überschrieben)
password: Passwort für die Steckdose (wird überschrieben) password: Passwort für die Steckdose (wird überschrieben)
timeout: Timeout in Sekunden (wird ignoriert, da PyP100 eigenes Timeout hat) timeout: Timeout in Sekunden (wird ignoriert, da PyP100 eigenes Timeout hat)
printer_id: ID des zugehörigen Druckers (für Logging)
Returns: Returns:
bool: True wenn erfolgreich ausgeschaltet bool: True wenn erfolgreich ausgeschaltet
""" """
if not TAPO_AVAILABLE: if not TAPO_AVAILABLE:
monitor_logger.error("⚠️ PyP100-Modul nicht verfügbar - kann Tapo-Steckdose nicht schalten") monitor_logger.error("⚠️ PyP100-Modul nicht verfügbar - kann Tapo-Steckdose nicht schalten")
# Logging: Fehlgeschlagener Versuch
if printer_id:
try:
PlugStatusLog.log_status_change(
printer_id=printer_id,
status="disconnected",
source="system",
ip_address=ip_address,
error_message="PyP100-Modul nicht verfügbar",
notes="Startup-Initialisierung fehlgeschlagen"
)
except Exception as log_error:
monitor_logger.warning(f"Fehler beim Loggen des Steckdosen-Status: {log_error}")
return False return False
# IMMER globale Anmeldedaten verwenden (da diese funktionieren) # IMMER globale Anmeldedaten verwenden (da diese funktionieren)
username = TAPO_USERNAME username = TAPO_USERNAME
password = TAPO_PASSWORD password = TAPO_PASSWORD
monitor_logger.debug(f"🔧 Verwende globale Tapo-Anmeldedaten für {ip_address}") monitor_logger.debug(f"🔧 Verwende globale Tapo-Anmeldedaten für {ip_address}")
start_time = time.time()
try: try:
# TP-Link Tapo P100 Verbindung herstellen (P100 statt P110) # TP-Link Tapo P100 Verbindung herstellen (P100 statt P110)
from PyP100 import PyP100 from PyP100 import PyP100
@ -169,11 +186,45 @@ class PrinterMonitor:
# Steckdose ausschalten # Steckdose ausschalten
p100.turnOff() p100.turnOff()
response_time = int((time.time() - start_time) * 1000) # in Millisekunden
monitor_logger.debug(f"✅ Tapo-Steckdose {ip_address} erfolgreich ausgeschaltet") monitor_logger.debug(f"✅ Tapo-Steckdose {ip_address} erfolgreich ausgeschaltet")
# Logging: Erfolgreich ausgeschaltet
if printer_id:
try:
PlugStatusLog.log_status_change(
printer_id=printer_id,
status="off",
source="system",
ip_address=ip_address,
response_time_ms=response_time,
notes="Startup-Initialisierung: Steckdose ausgeschaltet"
)
except Exception as log_error:
monitor_logger.warning(f"Fehler beim Loggen des Steckdosen-Status: {log_error}")
return True return True
except Exception as e: except Exception as e:
response_time = int((time.time() - start_time) * 1000) # in Millisekunden
monitor_logger.debug(f"⚠️ Fehler beim Ausschalten der Tapo-Steckdose {ip_address}: {str(e)}") monitor_logger.debug(f"⚠️ Fehler beim Ausschalten der Tapo-Steckdose {ip_address}: {str(e)}")
# Logging: Fehlgeschlagener Versuch
if printer_id:
try:
PlugStatusLog.log_status_change(
printer_id=printer_id,
status="disconnected",
source="system",
ip_address=ip_address,
response_time_ms=response_time,
error_message=str(e),
notes="Startup-Initialisierung fehlgeschlagen"
)
except Exception as log_error:
monitor_logger.warning(f"Fehler beim Loggen des Steckdosen-Status: {log_error}")
return False return False
def get_live_printer_status(self, use_session_cache: bool = True) -> Dict[int, Dict]: def get_live_printer_status(self, use_session_cache: bool = True) -> Dict[int, Dict]:
@ -337,7 +388,8 @@ class PrinterMonitor:
printer.plug_ip, printer.plug_ip,
printer.plug_username, printer.plug_username,
printer.plug_password, printer.plug_password,
timeout timeout,
printer_id=printer.id
) )
status_info["outlet_reachable"] = outlet_reachable status_info["outlet_reachable"] = outlet_reachable
@ -432,7 +484,7 @@ class PrinterMonitor:
monitor_logger.debug(f"❌ Fehler beim Verbindungstest zu {ip_address}: {str(e)}") monitor_logger.debug(f"❌ Fehler beim Verbindungstest zu {ip_address}: {str(e)}")
return False return False
def _check_outlet_status(self, ip_address: str, username: str, password: str, timeout: int = 5) -> Tuple[bool, str]: def _check_outlet_status(self, ip_address: str, username: str, password: str, timeout: int = 5, printer_id: int = None) -> Tuple[bool, str]:
""" """
Überprüft den Status einer TP-Link Tapo P110-Steckdose. Überprüft den Status einer TP-Link Tapo P110-Steckdose.
@ -441,19 +493,37 @@ class PrinterMonitor:
username: Benutzername für die Steckdose username: Benutzername für die Steckdose
password: Passwort für die Steckdose password: Passwort für die Steckdose
timeout: Timeout in Sekunden (wird ignoriert, da PyP100 eigenes Timeout hat) timeout: Timeout in Sekunden (wird ignoriert, da PyP100 eigenes Timeout hat)
printer_id: ID des zugehörigen Druckers (für Logging)
Returns: Returns:
Tuple[bool, str]: (Erreichbar, Status) - Status: "on", "off", "unknown" Tuple[bool, str]: (Erreichbar, Status) - Status: "on", "off", "unknown"
""" """
if not TAPO_AVAILABLE: if not TAPO_AVAILABLE:
monitor_logger.debug("⚠️ PyP100-Modul nicht verfügbar - kann Tapo-Steckdosen-Status nicht abfragen") monitor_logger.debug("⚠️ PyP100-Modul nicht verfügbar - kann Tapo-Steckdosen-Status nicht abfragen")
# Logging: Modul nicht verfügbar
if printer_id:
try:
PlugStatusLog.log_status_change(
printer_id=printer_id,
status="disconnected",
source="system",
ip_address=ip_address,
error_message="PyP100-Modul nicht verfügbar",
notes="Status-Check fehlgeschlagen"
)
except Exception as log_error:
monitor_logger.warning(f"Fehler beim Loggen des Steckdosen-Status: {log_error}")
return False, "unknown" return False, "unknown"
# IMMER globale Anmeldedaten verwenden (da diese funktionieren) # IMMER globale Anmeldedaten verwenden (da diese funktionieren)
username = TAPO_USERNAME username = TAPO_USERNAME
password = TAPO_PASSWORD password = TAPO_PASSWORD
monitor_logger.debug(f"🔧 Verwende globale Tapo-Anmeldedaten für {ip_address}") monitor_logger.debug(f"🔧 Verwende globale Tapo-Anmeldedaten für {ip_address}")
start_time = time.time()
try: try:
# TP-Link Tapo P100 Verbindung herstellen (P100 statt P110) # TP-Link Tapo P100 Verbindung herstellen (P100 statt P110)
from PyP100 import PyP100 from PyP100 import PyP100
@ -468,11 +538,70 @@ class PrinterMonitor:
device_on = device_info.get('device_on', False) device_on = device_info.get('device_on', False)
status = "on" if device_on else "off" status = "on" if device_on else "off"
response_time = int((time.time() - start_time) * 1000) # in Millisekunden
monitor_logger.debug(f"✅ Tapo-Steckdose {ip_address}: Status = {status}") monitor_logger.debug(f"✅ Tapo-Steckdose {ip_address}: Status = {status}")
# Logging: Erfolgreicher Status-Check
if printer_id:
try:
# Hole zusätzliche Geräteinformationen falls verfügbar
power_consumption = None
voltage = None
current = None
firmware_version = None
try:
# Versuche Energiedaten zu holen (P110 spezifisch)
energy_usage = p100.getEnergyUsage()
if energy_usage:
power_consumption = energy_usage.get('current_power', None)
voltage = energy_usage.get('voltage', None)
current = energy_usage.get('current', None)
except:
pass # P100 unterstützt keine Energiedaten
try:
firmware_version = device_info.get('fw_ver', None)
except:
pass
PlugStatusLog.log_status_change(
printer_id=printer_id,
status=status,
source="system",
ip_address=ip_address,
power_consumption=power_consumption,
voltage=voltage,
current=current,
response_time_ms=response_time,
firmware_version=firmware_version,
notes="Automatischer Status-Check"
)
except Exception as log_error:
monitor_logger.warning(f"Fehler beim Loggen des Steckdosen-Status: {log_error}")
return True, status return True, status
except Exception as e: except Exception as e:
response_time = int((time.time() - start_time) * 1000) # in Millisekunden
monitor_logger.debug(f"⚠️ Fehler bei Tapo-Steckdosen-Status-Check {ip_address}: {str(e)}") monitor_logger.debug(f"⚠️ Fehler bei Tapo-Steckdosen-Status-Check {ip_address}: {str(e)}")
# Logging: Fehlgeschlagener Status-Check
if printer_id:
try:
PlugStatusLog.log_status_change(
printer_id=printer_id,
status="disconnected",
source="system",
ip_address=ip_address,
response_time_ms=response_time,
error_message=str(e),
notes="Status-Check fehlgeschlagen"
)
except Exception as log_error:
monitor_logger.warning(f"Fehler beim Loggen des Steckdosen-Status: {log_error}")
return False, "unknown" return False, "unknown"
def clear_all_caches(self): def clear_all_caches(self):