🎉 Improved backend configuration and documentation 🖥️📚
This commit is contained in:
parent
3a0bd3b554
commit
f2928b97fc
@ -7,3 +7,5 @@ ca[]=
|
||||
cafile=/etc/ssl/certs/ca-certificates.crt
|
||||
strict-ssl=true
|
||||
registry=https://registry.npmjs.org/
|
||||
progress=false
|
||||
loglevel=warn
|
||||
|
230
backend/app.py
230
backend/app.py
@ -8367,6 +8367,236 @@ def refresh_dashboard():
|
||||
'details': str(e) if app.debug else None
|
||||
}), 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 =====
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
@ -6,6 +6,7 @@ from sqlalchemy import and_, or_, func
|
||||
|
||||
from models import Job, Printer, User, UserPermission, get_cached_session
|
||||
from utils.logging_config import get_logger
|
||||
from utils.conflict_manager import conflict_manager, ConflictType, ConflictSeverity
|
||||
|
||||
calendar_blueprint = Blueprint('calendar', __name__)
|
||||
logger = get_logger("calendar")
|
||||
@ -839,3 +840,479 @@ def api_export_calendar():
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Kalender-Export: {str(e)}")
|
||||
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
|
1
backend/docs/DRUCKERKONFLIKT_MANAGEMENT.md
Normal file
1
backend/docs/DRUCKERKONFLIKT_MANAGEMENT.md
Normal file
@ -0,0 +1 @@
|
||||
|
1
backend/docs/STECKDOSEN_MONITORING.md
Normal file
1
backend/docs/STECKDOSEN_MONITORING.md
Normal file
@ -0,0 +1 @@
|
||||
|
585
backend/templates/admin_plug_monitoring.html
Normal file
585
backend/templates/admin_plug_monitoring.html
Normal 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 %}
|
@ -714,6 +714,15 @@
|
||||
<span class="text-green-500 dark:text-green-400 font-medium transition-colors duration-300">Online</span>
|
||||
</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>
|
||||
<!-- Do Not Disturb Controls -->
|
||||
|
625
backend/utils/conflict_manager.py
Normal file
625
backend/utils/conflict_manager.py
Normal 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()
|
@ -15,7 +15,7 @@ from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
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 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(
|
||||
printer.plug_ip,
|
||||
printer.plug_username,
|
||||
printer.plug_password
|
||||
printer.plug_password,
|
||||
printer_id=printer.id
|
||||
)
|
||||
|
||||
results[printer.name] = success
|
||||
@ -138,7 +139,7 @@ class PrinterMonitor:
|
||||
|
||||
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.
|
||||
|
||||
@ -147,12 +148,26 @@ class PrinterMonitor:
|
||||
username: Benutzername 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)
|
||||
printer_id: ID des zugehörigen Druckers (für Logging)
|
||||
|
||||
Returns:
|
||||
bool: True wenn erfolgreich ausgeschaltet
|
||||
"""
|
||||
if not TAPO_AVAILABLE:
|
||||
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
|
||||
|
||||
# IMMER globale Anmeldedaten verwenden (da diese funktionieren)
|
||||
@ -160,6 +175,8 @@ class PrinterMonitor:
|
||||
password = TAPO_PASSWORD
|
||||
monitor_logger.debug(f"🔧 Verwende globale Tapo-Anmeldedaten für {ip_address}")
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# TP-Link Tapo P100 Verbindung herstellen (P100 statt P110)
|
||||
from PyP100 import PyP100
|
||||
@ -169,11 +186,45 @@ class PrinterMonitor:
|
||||
|
||||
# Steckdose ausschalten
|
||||
p100.turnOff()
|
||||
|
||||
response_time = int((time.time() - start_time) * 1000) # in Millisekunden
|
||||
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
|
||||
|
||||
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)}")
|
||||
|
||||
# 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
|
||||
|
||||
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_username,
|
||||
printer.plug_password,
|
||||
timeout
|
||||
timeout,
|
||||
printer_id=printer.id
|
||||
)
|
||||
|
||||
status_info["outlet_reachable"] = outlet_reachable
|
||||
@ -432,7 +484,7 @@ class PrinterMonitor:
|
||||
monitor_logger.debug(f"❌ Fehler beim Verbindungstest zu {ip_address}: {str(e)}")
|
||||
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.
|
||||
|
||||
@ -441,12 +493,28 @@ class PrinterMonitor:
|
||||
username: Benutzername für die Steckdose
|
||||
password: Passwort für die Steckdose
|
||||
timeout: Timeout in Sekunden (wird ignoriert, da PyP100 eigenes Timeout hat)
|
||||
printer_id: ID des zugehörigen Druckers (für Logging)
|
||||
|
||||
Returns:
|
||||
Tuple[bool, str]: (Erreichbar, Status) - Status: "on", "off", "unknown"
|
||||
"""
|
||||
if not TAPO_AVAILABLE:
|
||||
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"
|
||||
|
||||
# IMMER globale Anmeldedaten verwenden (da diese funktionieren)
|
||||
@ -454,6 +522,8 @@ class PrinterMonitor:
|
||||
password = TAPO_PASSWORD
|
||||
monitor_logger.debug(f"🔧 Verwende globale Tapo-Anmeldedaten für {ip_address}")
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# TP-Link Tapo P100 Verbindung herstellen (P100 statt P110)
|
||||
from PyP100 import PyP100
|
||||
@ -468,11 +538,70 @@ class PrinterMonitor:
|
||||
device_on = device_info.get('device_on', False)
|
||||
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}")
|
||||
|
||||
# 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
|
||||
|
||||
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)}")
|
||||
|
||||
# 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"
|
||||
|
||||
def clear_all_caches(self):
|
||||
|
Loading…
x
Reference in New Issue
Block a user