🎉 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
|
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
|
||||||
|
230
backend/app.py
230
backend/app.py
@ -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
|
||||||
|
@ -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
|
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>
|
<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 -->
|
||||||
|
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
|
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):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user