🆗 🚀 📚 Removed unused utility files for code optimization. 🎉🔧📚💄
This commit is contained in:
@ -1,178 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Skript zum Hinzufügen von Testdruckern zur Datenbank
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
# Füge das Anwendungsverzeichnis zum Python-Pfad hinzu
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from models import get_db_session, Printer
|
||||
|
||||
def add_test_printers():
|
||||
"""Fügt Testdrucker zur Datenbank hinzu"""
|
||||
|
||||
test_printers = [
|
||||
{
|
||||
"name": "Prusa i3 MK3S+",
|
||||
"model": "Prusa i3 MK3S+",
|
||||
"location": "Labor A - Arbeitsplatz 1",
|
||||
"mac_address": "AA:BB:CC:DD:EE:01",
|
||||
"plug_ip": "192.168.1.101",
|
||||
"status": "available",
|
||||
"active": True
|
||||
},
|
||||
{
|
||||
"name": "Ender 3 V2",
|
||||
"model": "Creality Ender 3 V2",
|
||||
"location": "Labor A - Arbeitsplatz 2",
|
||||
"mac_address": "AA:BB:CC:DD:EE:02",
|
||||
"plug_ip": "192.168.1.102",
|
||||
"status": "available",
|
||||
"active": True
|
||||
},
|
||||
{
|
||||
"name": "Ultimaker S3",
|
||||
"model": "Ultimaker S3",
|
||||
"location": "Labor B - Arbeitsplatz 1",
|
||||
"mac_address": "AA:BB:CC:DD:EE:03",
|
||||
"plug_ip": "192.168.1.103",
|
||||
"status": "available",
|
||||
"active": True
|
||||
},
|
||||
{
|
||||
"name": "Bambu Lab X1 Carbon",
|
||||
"model": "Bambu Lab X1 Carbon",
|
||||
"location": "Labor B - Arbeitsplatz 2",
|
||||
"mac_address": "AA:BB:CC:DD:EE:04",
|
||||
"plug_ip": "192.168.1.104",
|
||||
"status": "available",
|
||||
"active": True
|
||||
},
|
||||
{
|
||||
"name": "Formlabs Form 3",
|
||||
"model": "Formlabs Form 3",
|
||||
"location": "Labor C - Harz-Bereich",
|
||||
"mac_address": "AA:BB:CC:DD:EE:05",
|
||||
"plug_ip": "192.168.1.105",
|
||||
"status": "offline",
|
||||
"active": False
|
||||
}
|
||||
]
|
||||
|
||||
db_session = get_db_session()
|
||||
|
||||
try:
|
||||
added_count = 0
|
||||
|
||||
for printer_data in test_printers:
|
||||
# Prüfen, ob Drucker bereits existiert
|
||||
existing = db_session.query(Printer).filter(
|
||||
Printer.name == printer_data["name"]
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
print(f"⚠️ Drucker '{printer_data['name']}' existiert bereits - überspringe")
|
||||
continue
|
||||
|
||||
# Neuen Drucker erstellen
|
||||
new_printer = Printer(
|
||||
name=printer_data["name"],
|
||||
model=printer_data["model"],
|
||||
location=printer_data["location"],
|
||||
mac_address=printer_data["mac_address"],
|
||||
plug_ip=printer_data["plug_ip"],
|
||||
status=printer_data["status"],
|
||||
active=printer_data["active"],
|
||||
created_at=datetime.now()
|
||||
)
|
||||
|
||||
db_session.add(new_printer)
|
||||
added_count += 1
|
||||
print(f"✅ Drucker '{printer_data['name']}' hinzugefügt")
|
||||
|
||||
if added_count > 0:
|
||||
db_session.commit()
|
||||
print(f"\n🎉 {added_count} Testdrucker erfolgreich zur Datenbank hinzugefügt!")
|
||||
else:
|
||||
print("\n📋 Alle Testdrucker existieren bereits in der Datenbank")
|
||||
|
||||
# Zeige alle Drucker in der Datenbank
|
||||
all_printers = db_session.query(Printer).all()
|
||||
print(f"\n📊 Gesamt {len(all_printers)} Drucker in der Datenbank:")
|
||||
print("-" * 80)
|
||||
print(f"{'ID':<4} {'Name':<20} {'Modell':<20} {'Status':<12} {'Aktiv':<6}")
|
||||
print("-" * 80)
|
||||
|
||||
for printer in all_printers:
|
||||
active_str = "✅" if printer.active else "❌"
|
||||
print(f"{printer.id:<4} {printer.name[:19]:<20} {(printer.model or 'Unbekannt')[:19]:<20} {printer.status:<12} {active_str:<6}")
|
||||
|
||||
db_session.close()
|
||||
|
||||
except Exception as e:
|
||||
db_session.rollback()
|
||||
db_session.close()
|
||||
print(f"❌ Fehler beim Hinzufügen der Testdrucker: {str(e)}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def remove_test_printers():
|
||||
"""Entfernt alle Testdrucker aus der Datenbank"""
|
||||
|
||||
test_printer_names = [
|
||||
"Prusa i3 MK3S+",
|
||||
"Ender 3 V2",
|
||||
"Ultimaker S3",
|
||||
"Bambu Lab X1 Carbon",
|
||||
"Formlabs Form 3"
|
||||
]
|
||||
|
||||
db_session = get_db_session()
|
||||
|
||||
try:
|
||||
removed_count = 0
|
||||
|
||||
for name in test_printer_names:
|
||||
printer = db_session.query(Printer).filter(Printer.name == name).first()
|
||||
if printer:
|
||||
db_session.delete(printer)
|
||||
removed_count += 1
|
||||
print(f"🗑️ Drucker '{name}' entfernt")
|
||||
|
||||
if removed_count > 0:
|
||||
db_session.commit()
|
||||
print(f"\n🧹 {removed_count} Testdrucker erfolgreich entfernt!")
|
||||
else:
|
||||
print("\n📋 Keine Testdrucker zum Entfernen gefunden")
|
||||
|
||||
db_session.close()
|
||||
|
||||
except Exception as e:
|
||||
db_session.rollback()
|
||||
db_session.close()
|
||||
print(f"❌ Fehler beim Entfernen der Testdrucker: {str(e)}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== MYP Druckerverwaltung - Testdrucker-Verwaltung ===")
|
||||
print()
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "--remove":
|
||||
print("Entferne Testdrucker...")
|
||||
remove_test_printers()
|
||||
else:
|
||||
print("Füge Testdrucker hinzu...")
|
||||
print("(Verwende --remove um Testdrucker zu entfernen)")
|
||||
print()
|
||||
add_test_printers()
|
||||
|
||||
print("\nFertig! 🚀")
|
@ -1,95 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
SSL-Zertifikat-Generator für die MYP-Plattform
|
||||
Erstellt selbstsignierte SSL-Zertifikate für die lokale Entwicklung
|
||||
"""
|
||||
|
||||
import os
|
||||
import datetime
|
||||
import sys
|
||||
|
||||
# Überprüfen, ob die notwendigen Pakete installiert sind
|
||||
try:
|
||||
from cryptography import x509
|
||||
from cryptography.x509.oid import NameOID
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption
|
||||
except ImportError:
|
||||
print("Fehler: Paket 'cryptography' nicht gefunden.")
|
||||
print("Bitte installieren Sie es mit: pip install cryptography")
|
||||
sys.exit(1)
|
||||
|
||||
def create_self_signed_cert(cert_path, key_path, hostname="localhost"):
|
||||
"""
|
||||
Erstellt ein selbstsigniertes SSL-Zertifikat mit dem angegebenen Hostnamen.
|
||||
|
||||
Args:
|
||||
cert_path: Pfad zur Zertifikatsdatei
|
||||
key_path: Pfad zur privaten Schlüsseldatei
|
||||
hostname: Hostname für das Zertifikat (Standard: localhost)
|
||||
"""
|
||||
# Verzeichnis erstellen, falls es nicht existiert
|
||||
cert_dir = os.path.dirname(cert_path)
|
||||
if cert_dir and not os.path.exists(cert_dir):
|
||||
os.makedirs(cert_dir, exist_ok=True)
|
||||
|
||||
# Privaten Schlüssel generieren
|
||||
private_key = rsa.generate_private_key(
|
||||
public_exponent=65537,
|
||||
key_size=2048,
|
||||
)
|
||||
|
||||
# Schlüsseldatei schreiben
|
||||
with open(key_path, "wb") as key_file:
|
||||
key_file.write(private_key.private_bytes(
|
||||
encoding=Encoding.PEM,
|
||||
format=PrivateFormat.TraditionalOpenSSL,
|
||||
encryption_algorithm=NoEncryption()
|
||||
))
|
||||
|
||||
# Name für das Zertifikat erstellen
|
||||
subject = issuer = x509.Name([
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, hostname),
|
||||
])
|
||||
|
||||
# Zertifikat erstellen
|
||||
cert = x509.CertificateBuilder().subject_name(
|
||||
subject
|
||||
).issuer_name(
|
||||
issuer
|
||||
).public_key(
|
||||
private_key.public_key()
|
||||
).serial_number(
|
||||
x509.random_serial_number()
|
||||
).not_valid_before(
|
||||
datetime.datetime.utcnow()
|
||||
).not_valid_after(
|
||||
datetime.datetime.utcnow() + datetime.timedelta(days=365)
|
||||
).add_extension(
|
||||
x509.SubjectAlternativeName([x509.DNSName(hostname)]),
|
||||
critical=False,
|
||||
).sign(private_key, hashes.SHA256())
|
||||
|
||||
# Zertifikatsdatei schreiben
|
||||
with open(cert_path, "wb") as cert_file:
|
||||
cert_file.write(cert.public_bytes(Encoding.PEM))
|
||||
|
||||
print(f"Selbstsigniertes SSL-Zertifikat für '{hostname}' erstellt:")
|
||||
print(f"Zertifikat: {cert_path}")
|
||||
print(f"Schlüssel: {key_path}")
|
||||
print(f"Gültig für 1 Jahr.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Erstellt selbstsignierte SSL-Zertifikate für die lokale Entwicklung")
|
||||
parser.add_argument("-c", "--cert", default="/home/user/Projektarbeit-MYP/backend/app/certs/myp.crt", help="Pfad zur Zertifikatsdatei")
|
||||
parser.add_argument("-k", "--key", default="/home/user/Projektarbeit-MYP/backend/app/certs/myp.key", help="Pfad zur Schlüsseldatei")
|
||||
parser.add_argument("-n", "--hostname", default="localhost", help="Hostname für das Zertifikat")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
create_self_signed_cert(args.cert, args.key, args.hostname)
|
@ -1,106 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script zum Erstellen von Test-Druckern für die MYP Plattform
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.append('.')
|
||||
|
||||
from models import *
|
||||
from datetime import datetime
|
||||
|
||||
def create_test_printers():
|
||||
"""Erstellt Test-Drucker in der Datenbank."""
|
||||
|
||||
# Verbindung zur Datenbank
|
||||
db_session = get_db_session()
|
||||
|
||||
# Test-Drucker Daten
|
||||
test_printers = [
|
||||
{
|
||||
'name': 'Mercedes-Benz FDM Pro #01',
|
||||
'model': 'Ultimaker S5 Pro',
|
||||
'location': 'Werkhalle Sindelfingen',
|
||||
'plug_ip': '192.168.10.101',
|
||||
'status': 'available',
|
||||
'active': True
|
||||
},
|
||||
{
|
||||
'name': 'Mercedes-Benz FDM #02',
|
||||
'model': 'Prusa MK3S+',
|
||||
'location': 'Entwicklungszentrum Stuttgart',
|
||||
'plug_ip': '192.168.10.102',
|
||||
'status': 'printing',
|
||||
'active': True
|
||||
},
|
||||
{
|
||||
'name': 'Mercedes-Benz SLA #01',
|
||||
'model': 'Formlabs Form 3+',
|
||||
'location': 'Prototypenlabor',
|
||||
'plug_ip': '192.168.10.103',
|
||||
'status': 'available',
|
||||
'active': True
|
||||
},
|
||||
{
|
||||
'name': 'Mercedes-Benz Industrial #01',
|
||||
'model': 'Stratasys F370',
|
||||
'location': 'Industriehalle Bremen',
|
||||
'plug_ip': '192.168.10.104',
|
||||
'status': 'maintenance',
|
||||
'active': False
|
||||
},
|
||||
{
|
||||
'name': 'Mercedes-Benz Rapid #01',
|
||||
'model': 'Bambu Lab X1 Carbon',
|
||||
'location': 'Designabteilung',
|
||||
'plug_ip': '192.168.10.105',
|
||||
'status': 'offline',
|
||||
'active': True
|
||||
},
|
||||
{
|
||||
'name': 'Mercedes-Benz SLS #01',
|
||||
'model': 'HP Jet Fusion 5200',
|
||||
'location': 'Produktionszentrum Berlin',
|
||||
'plug_ip': '192.168.10.106',
|
||||
'status': 'available',
|
||||
'active': True
|
||||
}
|
||||
]
|
||||
try:
|
||||
created_count = 0
|
||||
for printer_data in test_printers:
|
||||
# Prüfen ob Drucker bereits existiert
|
||||
existing = db_session.query(Printer).filter_by(name=printer_data['name']).first()
|
||||
if not existing:
|
||||
printer = Printer(
|
||||
name=printer_data['name'],
|
||||
model=printer_data['model'],
|
||||
location=printer_data['location'],
|
||||
plug_ip=printer_data['plug_ip'],
|
||||
status=printer_data['status'],
|
||||
active=printer_data['active'],
|
||||
created_at=datetime.now()
|
||||
)
|
||||
db_session.add(printer)
|
||||
created_count += 1
|
||||
print(f"✅ Drucker '{printer_data['name']}' erstellt")
|
||||
else:
|
||||
print(f"ℹ️ Drucker '{printer_data['name']}' existiert bereits")
|
||||
|
||||
db_session.commit()
|
||||
|
||||
total_count = db_session.query(Printer).count()
|
||||
print(f"\n🎉 {created_count} neue Test-Drucker erstellt!")
|
||||
print(f"📊 Insgesamt {total_count} Drucker in der Datenbank.")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Fehler beim Erstellen der Test-Drucker: {str(e)}")
|
||||
db_session.rollback()
|
||||
finally:
|
||||
db_session.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🚀 Erstelle Test-Drucker für MYP Plattform...")
|
||||
create_test_printers()
|
||||
print("✅ Fertig!")
|
@ -1,175 +0,0 @@
|
||||
"""
|
||||
Offline-kompatible E-Mail-Benachrichtigung für MYP-System
|
||||
========================================================
|
||||
|
||||
Da das System im Produktionsbetrieb offline läuft, werden alle E-Mail-Benachrichtigungen
|
||||
nur geloggt aber nicht tatsächlich versendet.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
from utils.logging_config import get_logger
|
||||
|
||||
logger = get_logger("email_notification")
|
||||
|
||||
class OfflineEmailNotification:
|
||||
"""
|
||||
Offline-E-Mail-Benachrichtigung die nur Logs erstellt.
|
||||
Simuliert E-Mail-Versand für Offline-Betrieb.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.enabled = False # Immer deaktiviert im Offline-Modus
|
||||
logger.info("📧 Offline-E-Mail-Benachrichtigung initialisiert (kein echter E-Mail-Versand)")
|
||||
|
||||
def send_email(self, to: str, subject: str, body: str, **kwargs) -> bool:
|
||||
"""
|
||||
Simuliert E-Mail-Versand durch Logging.
|
||||
|
||||
Args:
|
||||
to: E-Mail-Empfänger
|
||||
subject: E-Mail-Betreff
|
||||
body: E-Mail-Inhalt
|
||||
**kwargs: Zusätzliche Parameter
|
||||
|
||||
Returns:
|
||||
bool: Immer True (Simulation erfolgreich)
|
||||
"""
|
||||
logger.info(f"📧 [OFFLINE-SIMULATION] E-Mail würde versendet werden:")
|
||||
logger.info(f" 📮 An: {to}")
|
||||
logger.info(f" 📋 Betreff: {subject}")
|
||||
logger.info(f" 📝 Inhalt: {body[:100]}{'...' if len(body) > 100 else ''}")
|
||||
logger.info(f" 🕒 Zeitpunkt: {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}")
|
||||
|
||||
if kwargs:
|
||||
logger.info(f" ⚙️ Zusätzliche Parameter: {kwargs}")
|
||||
|
||||
return True
|
||||
|
||||
def send_notification_email(self, recipient: str, notification_type: str,
|
||||
data: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Sendet Benachrichtigungs-E-Mail (Offline-Simulation).
|
||||
|
||||
Args:
|
||||
recipient: E-Mail-Empfänger
|
||||
notification_type: Art der Benachrichtigung
|
||||
data: Daten für die Benachrichtigung
|
||||
|
||||
Returns:
|
||||
bool: Immer True (Simulation erfolgreich)
|
||||
"""
|
||||
subject = f"MYP-Benachrichtigung: {notification_type}"
|
||||
body = f"Benachrichtigung vom MYP-System:\n\n{data}"
|
||||
|
||||
return self.send_email(recipient, subject, body, notification_type=notification_type)
|
||||
|
||||
def send_maintenance_notification(self, recipient: str, task_title: str,
|
||||
task_description: str) -> bool:
|
||||
"""
|
||||
Sendet Wartungs-Benachrichtigung (Offline-Simulation).
|
||||
|
||||
Args:
|
||||
recipient: E-Mail-Empfänger
|
||||
task_title: Titel der Wartungsaufgabe
|
||||
task_description: Beschreibung der Wartungsaufgabe
|
||||
|
||||
Returns:
|
||||
bool: Immer True (Simulation erfolgreich)
|
||||
"""
|
||||
subject = f"MYP-Wartungsaufgabe: {task_title}"
|
||||
body = f"""
|
||||
Neue Wartungsaufgabe im MYP-System:
|
||||
|
||||
Titel: {task_title}
|
||||
Beschreibung: {task_description}
|
||||
Erstellt: {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}
|
||||
|
||||
Bitte loggen Sie sich in das MYP-System ein, um weitere Details zu sehen.
|
||||
"""
|
||||
|
||||
return self.send_email(recipient, subject, body, task_type="maintenance")
|
||||
|
||||
# Globale Instanz für einfache Verwendung
|
||||
email_notifier = OfflineEmailNotification()
|
||||
|
||||
def send_email_notification(recipient: str, subject: str, body: str, **kwargs) -> bool:
|
||||
"""
|
||||
Haupt-Funktion für E-Mail-Versand (Offline-kompatibel).
|
||||
|
||||
Args:
|
||||
recipient: E-Mail-Empfänger
|
||||
subject: E-Mail-Betreff
|
||||
body: E-Mail-Inhalt
|
||||
**kwargs: Zusätzliche Parameter
|
||||
|
||||
Returns:
|
||||
bool: True wenn "erfolgreich" (geloggt)
|
||||
"""
|
||||
return email_notifier.send_email(recipient, subject, body, **kwargs)
|
||||
|
||||
def send_maintenance_email(recipient: str, task_title: str, task_description: str) -> bool:
|
||||
"""
|
||||
Sendet Wartungs-E-Mail (Offline-kompatibel).
|
||||
|
||||
Args:
|
||||
recipient: E-Mail-Empfänger
|
||||
task_title: Titel der Wartungsaufgabe
|
||||
task_description: Beschreibung der Wartungsaufgabe
|
||||
|
||||
Returns:
|
||||
bool: True wenn "erfolgreich" (geloggt)
|
||||
"""
|
||||
return email_notifier.send_maintenance_notification(recipient, task_title, task_description)
|
||||
|
||||
def send_guest_approval_email(recipient: str, otp_code: str, expires_at: str) -> bool:
|
||||
"""
|
||||
Sendet Gastauftrags-Genehmigung-E-Mail (Offline-kompatibel).
|
||||
|
||||
Args:
|
||||
recipient: E-Mail-Empfänger
|
||||
otp_code: OTP-Code für den Gastauftrag
|
||||
expires_at: Ablaufzeit des OTP-Codes
|
||||
|
||||
Returns:
|
||||
bool: True wenn "erfolgreich" (geloggt)
|
||||
"""
|
||||
subject = "MYP-Gastauftrag genehmigt"
|
||||
body = f"""
|
||||
Ihr Gastauftrag wurde genehmigt!
|
||||
|
||||
OTP-Code: {otp_code}
|
||||
Gültig bis: {expires_at}
|
||||
|
||||
Bitte verwenden Sie diesen Code am MYP-Terminal, um Ihren Druckauftrag zu starten.
|
||||
"""
|
||||
|
||||
return email_notifier.send_email(recipient, subject, body,
|
||||
otp_code=otp_code, expires_at=expires_at)
|
||||
|
||||
def send_guest_rejection_email(recipient: str, reason: str) -> bool:
|
||||
"""
|
||||
Sendet Gastauftrags-Ablehnungs-E-Mail (Offline-kompatibel).
|
||||
|
||||
Args:
|
||||
recipient: E-Mail-Empfänger
|
||||
reason: Grund für die Ablehnung
|
||||
|
||||
Returns:
|
||||
bool: True wenn "erfolgreich" (geloggt)
|
||||
"""
|
||||
subject = "MYP-Gastauftrag abgelehnt"
|
||||
body = f"""
|
||||
Ihr Gastauftrag wurde leider abgelehnt.
|
||||
|
||||
Grund: {reason}
|
||||
|
||||
Bei Fragen wenden Sie sich bitte an das MYP-Team.
|
||||
"""
|
||||
|
||||
return email_notifier.send_email(recipient, subject, body, rejection_reason=reason)
|
||||
|
||||
# Für Backward-Kompatibilität
|
||||
send_notification = send_email_notification
|
@ -1,790 +0,0 @@
|
||||
"""
|
||||
Wartungsplanungs- und Tracking-System für das MYP-System
|
||||
========================================================
|
||||
|
||||
Dieses Modul stellt umfassende Wartungsfunktionalität bereit:
|
||||
- Geplante und ungeplante Wartungen
|
||||
- Wartungsintervalle und Erinnerungen
|
||||
- Wartungshistorie und Berichte
|
||||
- Automatische Wartungsprüfungen
|
||||
- Ersatzteil-Management
|
||||
- Techniker-Zuweisungen
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Any, Optional, Callable
|
||||
from dataclasses import dataclass, asdict
|
||||
from enum import Enum
|
||||
import threading
|
||||
import schedule
|
||||
import time
|
||||
|
||||
from utils.logging_config import get_logger
|
||||
from models import Printer, get_db_session
|
||||
from utils.email_notification import send_email_notification
|
||||
from utils.realtime_dashboard import emit_system_alert
|
||||
|
||||
logger = get_logger("maintenance")
|
||||
|
||||
class MaintenanceType(Enum):
|
||||
"""Arten von Wartungen"""
|
||||
PREVENTIVE = "preventive" # Vorbeugende Wartung
|
||||
CORRECTIVE = "corrective" # Reparatur/Korrektur
|
||||
EMERGENCY = "emergency" # Notfall-Wartung
|
||||
SCHEDULED = "scheduled" # Geplante Wartung
|
||||
INSPECTION = "inspection" # Inspektion
|
||||
|
||||
class MaintenanceStatus(Enum):
|
||||
"""Status einer Wartung"""
|
||||
PLANNED = "planned" # Geplant
|
||||
SCHEDULED = "scheduled" # Terminiert
|
||||
IN_PROGRESS = "in_progress" # In Bearbeitung
|
||||
COMPLETED = "completed" # Abgeschlossen
|
||||
CANCELLED = "cancelled" # Abgebrochen
|
||||
OVERDUE = "overdue" # Überfällig
|
||||
|
||||
class MaintenancePriority(Enum):
|
||||
"""Priorität einer Wartung"""
|
||||
LOW = "low" # Niedrig
|
||||
NORMAL = "normal" # Normal
|
||||
HIGH = "high" # Hoch
|
||||
CRITICAL = "critical" # Kritisch
|
||||
EMERGENCY = "emergency" # Notfall
|
||||
|
||||
@dataclass
|
||||
class MaintenanceTask:
|
||||
"""Wartungsaufgabe"""
|
||||
id: Optional[int] = None
|
||||
printer_id: int = None
|
||||
title: str = ""
|
||||
description: str = ""
|
||||
maintenance_type: MaintenanceType = MaintenanceType.PREVENTIVE
|
||||
priority: MaintenancePriority = MaintenancePriority.NORMAL
|
||||
status: MaintenanceStatus = MaintenanceStatus.PLANNED
|
||||
scheduled_date: Optional[datetime] = None
|
||||
due_date: Optional[datetime] = None
|
||||
estimated_duration: int = 60 # Minuten
|
||||
actual_duration: Optional[int] = None
|
||||
assigned_technician: Optional[str] = None
|
||||
created_at: datetime = None
|
||||
started_at: Optional[datetime] = None
|
||||
completed_at: Optional[datetime] = None
|
||||
notes: str = ""
|
||||
required_parts: List[str] = None
|
||||
actual_parts_used: List[str] = None
|
||||
cost: Optional[float] = None
|
||||
checklist: List[Dict[str, Any]] = None
|
||||
photos: List[str] = None
|
||||
created_by: Optional[int] = None
|
||||
|
||||
@dataclass
|
||||
class MaintenanceSchedule:
|
||||
"""Wartungsplan"""
|
||||
printer_id: int
|
||||
maintenance_type: MaintenanceType
|
||||
interval_days: int
|
||||
next_due: datetime
|
||||
last_completed: Optional[datetime] = None
|
||||
is_active: bool = True
|
||||
description: str = ""
|
||||
checklist_template: List[str] = None
|
||||
|
||||
@dataclass
|
||||
class MaintenanceMetrics:
|
||||
"""Wartungsmetriken"""
|
||||
total_tasks: int = 0
|
||||
completed_tasks: int = 0
|
||||
overdue_tasks: int = 0
|
||||
average_completion_time: float = 0.0
|
||||
total_cost: float = 0.0
|
||||
mtbf: float = 0.0 # Mean Time Between Failures
|
||||
mttr: float = 0.0 # Mean Time To Repair
|
||||
uptime_percentage: float = 0.0
|
||||
|
||||
class MaintenanceManager:
|
||||
"""Manager für Wartungsplanung und -tracking"""
|
||||
|
||||
def __init__(self):
|
||||
self.tasks: Dict[int, MaintenanceTask] = {}
|
||||
self.schedules: Dict[int, List[MaintenanceSchedule]] = {}
|
||||
self.maintenance_history: List[MaintenanceTask] = []
|
||||
self.next_task_id = 1
|
||||
self.is_running = False
|
||||
|
||||
self._setup_scheduler()
|
||||
|
||||
def _setup_scheduler(self):
|
||||
"""Richtet automatische Wartungsplanung ein"""
|
||||
schedule.every().day.at("06:00").do(self._check_scheduled_maintenance)
|
||||
schedule.every().hour.do(self._check_overdue_tasks)
|
||||
schedule.every().monday.at("08:00").do(self._generate_weekly_report)
|
||||
|
||||
# Scheduler in separatem Thread
|
||||
def run_scheduler():
|
||||
while self.is_running:
|
||||
schedule.run_pending()
|
||||
time.sleep(60) # Check every minute
|
||||
|
||||
self.is_running = True
|
||||
scheduler_thread = threading.Thread(target=run_scheduler, daemon=True)
|
||||
scheduler_thread.start()
|
||||
|
||||
logger.info("Wartungs-Scheduler gestartet")
|
||||
|
||||
def create_task(self, task: MaintenanceTask) -> int:
|
||||
"""Erstellt eine neue Wartungsaufgabe"""
|
||||
task.id = self.next_task_id
|
||||
self.next_task_id += 1
|
||||
task.created_at = datetime.now()
|
||||
|
||||
self.tasks[task.id] = task
|
||||
|
||||
# Automatische Terminierung für vorbeugende Wartungen
|
||||
if task.maintenance_type == MaintenanceType.PREVENTIVE and not task.scheduled_date:
|
||||
task.scheduled_date = self._calculate_next_maintenance_date(task.printer_id)
|
||||
|
||||
# Benachrichtigungen senden
|
||||
self._send_task_notifications(task, "created")
|
||||
|
||||
logger.info(f"Wartungsaufgabe erstellt: {task.title} für Drucker {task.printer_id}")
|
||||
return task.id
|
||||
|
||||
def update_task_status(self, task_id: int, new_status: MaintenanceStatus, notes: str = "") -> bool:
|
||||
"""Aktualisiert den Status einer Wartungsaufgabe"""
|
||||
if task_id not in self.tasks:
|
||||
return False
|
||||
|
||||
task = self.tasks[task_id]
|
||||
old_status = task.status
|
||||
task.status = new_status
|
||||
|
||||
# Zeitstempel setzen
|
||||
if new_status == MaintenanceStatus.IN_PROGRESS:
|
||||
task.started_at = datetime.now()
|
||||
elif new_status == MaintenanceStatus.COMPLETED:
|
||||
task.completed_at = datetime.now()
|
||||
if task.started_at:
|
||||
task.actual_duration = int((task.completed_at - task.started_at).total_seconds() / 60)
|
||||
|
||||
# Zur Historie hinzufügen
|
||||
self.maintenance_history.append(task)
|
||||
|
||||
# Nächste Wartung planen
|
||||
self._schedule_next_maintenance(task)
|
||||
|
||||
if notes:
|
||||
task.notes += f"\n{datetime.now().strftime('%d.%m.%Y %H:%M')}: {notes}"
|
||||
|
||||
# Benachrichtigungen senden
|
||||
if old_status != new_status:
|
||||
self._send_task_notifications(task, "status_changed")
|
||||
|
||||
logger.info(f"Wartungsaufgabe {task_id} Status: {old_status.value} → {new_status.value}")
|
||||
return True
|
||||
|
||||
def schedule_maintenance(self, printer_id: int, maintenance_type: MaintenanceType,
|
||||
interval_days: int, description: str = "") -> MaintenanceSchedule:
|
||||
"""Plant regelmäßige Wartungen"""
|
||||
schedule_item = MaintenanceSchedule(
|
||||
printer_id=printer_id,
|
||||
maintenance_type=maintenance_type,
|
||||
interval_days=interval_days,
|
||||
next_due=datetime.now() + timedelta(days=interval_days),
|
||||
description=description
|
||||
)
|
||||
|
||||
if printer_id not in self.schedules:
|
||||
self.schedules[printer_id] = []
|
||||
|
||||
self.schedules[printer_id].append(schedule_item)
|
||||
|
||||
logger.info(f"Wartungsplan erstellt: {maintenance_type.value} alle {interval_days} Tage für Drucker {printer_id}")
|
||||
return schedule_item
|
||||
|
||||
def get_upcoming_maintenance(self, days_ahead: int = 7) -> List[MaintenanceTask]:
|
||||
"""Holt anstehende Wartungen"""
|
||||
cutoff_date = datetime.now() + timedelta(days=days_ahead)
|
||||
|
||||
upcoming = []
|
||||
for task in self.tasks.values():
|
||||
if (task.status in [MaintenanceStatus.PLANNED, MaintenanceStatus.SCHEDULED] and
|
||||
task.due_date and task.due_date <= cutoff_date):
|
||||
upcoming.append(task)
|
||||
|
||||
return sorted(upcoming, key=lambda t: t.due_date or datetime.max)
|
||||
|
||||
def get_overdue_tasks(self) -> List[MaintenanceTask]:
|
||||
"""Holt überfällige Wartungen"""
|
||||
now = datetime.now()
|
||||
overdue = []
|
||||
|
||||
for task in self.tasks.values():
|
||||
if (task.status in [MaintenanceStatus.PLANNED, MaintenanceStatus.SCHEDULED] and
|
||||
task.due_date and task.due_date < now):
|
||||
task.status = MaintenanceStatus.OVERDUE
|
||||
overdue.append(task)
|
||||
|
||||
return overdue
|
||||
|
||||
def get_maintenance_metrics(self, printer_id: Optional[int] = None,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None) -> MaintenanceMetrics:
|
||||
"""Berechnet Wartungsmetriken"""
|
||||
# Filter tasks
|
||||
tasks = self.maintenance_history.copy()
|
||||
if printer_id:
|
||||
tasks = [t for t in tasks if t.printer_id == printer_id]
|
||||
if start_date:
|
||||
tasks = [t for t in tasks if t.completed_at and t.completed_at >= start_date]
|
||||
if end_date:
|
||||
tasks = [t for t in tasks if t.completed_at and t.completed_at <= end_date]
|
||||
|
||||
if not tasks:
|
||||
return MaintenanceMetrics()
|
||||
|
||||
completed_tasks = [t for t in tasks if t.status == MaintenanceStatus.COMPLETED]
|
||||
|
||||
# Grundmetriken
|
||||
total_tasks = len(tasks)
|
||||
completed_count = len(completed_tasks)
|
||||
|
||||
# Durchschnittliche Bearbeitungszeit
|
||||
completion_times = [t.actual_duration for t in completed_tasks if t.actual_duration]
|
||||
avg_completion_time = sum(completion_times) / len(completion_times) if completion_times else 0
|
||||
|
||||
# Gesamtkosten
|
||||
total_cost = sum(t.cost for t in completed_tasks if t.cost)
|
||||
|
||||
# MTBF und MTTR berechnen
|
||||
mtbf = self._calculate_mtbf(tasks, printer_id)
|
||||
mttr = avg_completion_time / 60 # Konvertiere zu Stunden
|
||||
|
||||
# Verfügbarkeit berechnen
|
||||
uptime_percentage = self._calculate_uptime(printer_id, start_date, end_date)
|
||||
|
||||
return MaintenanceMetrics(
|
||||
total_tasks=total_tasks,
|
||||
completed_tasks=completed_count,
|
||||
overdue_tasks=len(self.get_overdue_tasks()),
|
||||
average_completion_time=avg_completion_time,
|
||||
total_cost=total_cost,
|
||||
mtbf=mtbf,
|
||||
mttr=mttr,
|
||||
uptime_percentage=uptime_percentage
|
||||
)
|
||||
|
||||
def create_maintenance_checklist(self, maintenance_type: MaintenanceType) -> List[Dict[str, Any]]:
|
||||
"""Erstellt eine Wartungs-Checkliste"""
|
||||
checklists = {
|
||||
MaintenanceType.PREVENTIVE: [
|
||||
{"task": "Drucker äußerlich reinigen", "completed": False, "required": True},
|
||||
{"task": "Druckbett-Level prüfen", "completed": False, "required": True},
|
||||
{"task": "Extruder-Düse reinigen", "completed": False, "required": True},
|
||||
{"task": "Riemen-Spannung prüfen", "completed": False, "required": True},
|
||||
{"task": "Filament-Führung prüfen", "completed": False, "required": False},
|
||||
{"task": "Software-Updates prüfen", "completed": False, "required": False},
|
||||
{"task": "Lüfter reinigen", "completed": False, "required": True},
|
||||
{"task": "Schrauben nachziehen", "completed": False, "required": False}
|
||||
],
|
||||
MaintenanceType.CORRECTIVE: [
|
||||
{"task": "Problem-Diagnose durchführen", "completed": False, "required": True},
|
||||
{"task": "Defekte Teile identifizieren", "completed": False, "required": True},
|
||||
{"task": "Ersatzteile bestellen/bereitstellen", "completed": False, "required": True},
|
||||
{"task": "Reparatur durchführen", "completed": False, "required": True},
|
||||
{"task": "Funktionstest durchführen", "completed": False, "required": True},
|
||||
{"task": "Kalibrierung prüfen", "completed": False, "required": True}
|
||||
],
|
||||
MaintenanceType.INSPECTION: [
|
||||
{"task": "Sichtprüfung der Mechanik", "completed": False, "required": True},
|
||||
{"task": "Druckqualität testen", "completed": False, "required": True},
|
||||
{"task": "Temperaturen prüfen", "completed": False, "required": True},
|
||||
{"task": "Bewegungen testen", "completed": False, "required": True},
|
||||
{"task": "Verschleiß bewerten", "completed": False, "required": True}
|
||||
]
|
||||
}
|
||||
|
||||
return checklists.get(maintenance_type, [])
|
||||
|
||||
def _check_scheduled_maintenance(self):
|
||||
"""Prüft täglich auf fällige Wartungen"""
|
||||
logger.info("Prüfe fällige Wartungen...")
|
||||
|
||||
today = datetime.now()
|
||||
|
||||
for printer_id, schedules in self.schedules.items():
|
||||
for schedule_item in schedules:
|
||||
if not schedule_item.is_active:
|
||||
continue
|
||||
|
||||
if schedule_item.next_due <= today:
|
||||
# Erstelle Wartungsaufgabe
|
||||
task = MaintenanceTask(
|
||||
printer_id=printer_id,
|
||||
title=f"{schedule_item.maintenance_type.value.title()} Wartung",
|
||||
description=schedule_item.description,
|
||||
maintenance_type=schedule_item.maintenance_type,
|
||||
priority=MaintenancePriority.NORMAL,
|
||||
due_date=schedule_item.next_due,
|
||||
checklist=self.create_maintenance_checklist(schedule_item.maintenance_type)
|
||||
)
|
||||
|
||||
task_id = self.create_task(task)
|
||||
|
||||
# Nächsten Termin berechnen
|
||||
schedule_item.next_due = today + timedelta(days=schedule_item.interval_days)
|
||||
|
||||
logger.info(f"Automatische Wartungsaufgabe erstellt: {task_id}")
|
||||
|
||||
def _check_overdue_tasks(self):
|
||||
"""Prüft stündlich auf überfällige Aufgaben"""
|
||||
overdue = self.get_overdue_tasks()
|
||||
|
||||
if overdue:
|
||||
logger.warning(f"{len(overdue)} überfällige Wartungsaufgaben gefunden")
|
||||
|
||||
for task in overdue:
|
||||
emit_system_alert(
|
||||
f"Wartung überfällig: {task.title} (Drucker {task.printer_id})",
|
||||
"warning",
|
||||
"high"
|
||||
)
|
||||
|
||||
def _generate_weekly_report(self):
|
||||
"""Generiert wöchentlichen Wartungsbericht"""
|
||||
logger.info("Generiere wöchentlichen Wartungsbericht...")
|
||||
|
||||
# Sammle Daten der letzten Woche
|
||||
last_week = datetime.now() - timedelta(days=7)
|
||||
metrics = self.get_maintenance_metrics(start_date=last_week)
|
||||
|
||||
# Sende Report (Implementation abhängig von verfügbaren Services)
|
||||
# send_maintenance_report(metrics)
|
||||
|
||||
def _calculate_next_maintenance_date(self, printer_id: int) -> datetime:
|
||||
"""Berechnet nächstes Wartungsdatum basierend auf Nutzung"""
|
||||
# Vereinfachte Implementierung - kann erweitert werden
|
||||
base_interval = 30 # Tage
|
||||
|
||||
# Hier könnte man Nutzungsstatistiken einbeziehen
|
||||
with get_db_session() as db_session:
|
||||
printer = db_session.query(Printer).filter(Printer.id == printer_id).first()
|
||||
if printer:
|
||||
# Berücksichtige letzten Check
|
||||
if printer.last_checked:
|
||||
days_since_check = (datetime.now() - printer.last_checked).days
|
||||
if days_since_check < 15: # Kürzlich gecheckt
|
||||
base_interval += 15
|
||||
|
||||
return datetime.now() + timedelta(days=base_interval)
|
||||
|
||||
def _schedule_next_maintenance(self, completed_task: MaintenanceTask):
|
||||
"""Plant nächste Wartung nach Abschluss einer Aufgabe"""
|
||||
if completed_task.maintenance_type == MaintenanceType.PREVENTIVE:
|
||||
# Finde entsprechenden Schedule
|
||||
printer_schedules = self.schedules.get(completed_task.printer_id, [])
|
||||
for schedule_item in printer_schedules:
|
||||
if schedule_item.maintenance_type == completed_task.maintenance_type:
|
||||
schedule_item.last_completed = completed_task.completed_at
|
||||
schedule_item.next_due = datetime.now() + timedelta(days=schedule_item.interval_days)
|
||||
break
|
||||
|
||||
def _calculate_mtbf(self, tasks: List[MaintenanceTask], printer_id: Optional[int]) -> float:
|
||||
"""Berechnet Mean Time Between Failures"""
|
||||
# Vereinfachte MTBF-Berechnung
|
||||
failure_tasks = [t for t in tasks if t.maintenance_type == MaintenanceType.CORRECTIVE]
|
||||
|
||||
if len(failure_tasks) < 2:
|
||||
return 0.0
|
||||
|
||||
# Zeitspanne zwischen ersten und letzten Ausfall
|
||||
first_failure = min(failure_tasks, key=lambda t: t.created_at)
|
||||
last_failure = max(failure_tasks, key=lambda t: t.created_at)
|
||||
|
||||
total_time = (last_failure.created_at - first_failure.created_at).total_seconds() / 3600 # Stunden
|
||||
failure_count = len(failure_tasks) - 1
|
||||
|
||||
return total_time / failure_count if failure_count > 0 else 0.0
|
||||
|
||||
def _calculate_uptime(self, printer_id: Optional[int], start_date: Optional[datetime],
|
||||
end_date: Optional[datetime]) -> float:
|
||||
"""Berechnet Verfügbarkeit in Prozent"""
|
||||
# Vereinfachte Uptime-Berechnung
|
||||
if not start_date:
|
||||
start_date = datetime.now() - timedelta(days=30)
|
||||
if not end_date:
|
||||
end_date = datetime.now()
|
||||
|
||||
total_time = (end_date - start_date).total_seconds()
|
||||
|
||||
# Berechne Downtime aus Wartungszeiten
|
||||
downtime = 0
|
||||
for task in self.maintenance_history:
|
||||
if printer_id and task.printer_id != printer_id:
|
||||
continue
|
||||
|
||||
if (task.status == MaintenanceStatus.COMPLETED and
|
||||
task.started_at and task.completed_at and
|
||||
task.started_at >= start_date and task.completed_at <= end_date):
|
||||
downtime += (task.completed_at - task.started_at).total_seconds()
|
||||
|
||||
uptime = ((total_time - downtime) / total_time) * 100 if total_time > 0 else 0
|
||||
return max(0, min(100, uptime))
|
||||
|
||||
def _send_task_notifications(self, task: MaintenanceTask, event_type: str):
|
||||
"""Sendet Benachrichtigungen für Wartungsaufgaben"""
|
||||
try:
|
||||
if event_type == "created":
|
||||
emit_system_alert(
|
||||
f"Neue Wartungsaufgabe: {task.title} (Drucker {task.printer_id})",
|
||||
"info",
|
||||
"normal"
|
||||
)
|
||||
elif event_type == "status_changed":
|
||||
emit_system_alert(
|
||||
f"Wartungsstatus geändert: {task.title} → {task.status.value}",
|
||||
"info",
|
||||
"normal"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Senden der Wartungsbenachrichtigung: {str(e)}")
|
||||
|
||||
# Globale Instanz
|
||||
maintenance_manager = MaintenanceManager()
|
||||
|
||||
def get_maintenance_dashboard_data() -> Dict[str, Any]:
|
||||
"""Holt Dashboard-Daten für Wartungen"""
|
||||
upcoming = maintenance_manager.get_upcoming_maintenance()
|
||||
overdue = maintenance_manager.get_overdue_tasks()
|
||||
metrics = maintenance_manager.get_maintenance_metrics()
|
||||
|
||||
return {
|
||||
'upcoming_count': len(upcoming),
|
||||
'overdue_count': len(overdue),
|
||||
'upcoming_tasks': [asdict(task) for task in upcoming[:5]],
|
||||
'overdue_tasks': [asdict(task) for task in overdue],
|
||||
'metrics': asdict(metrics),
|
||||
'next_scheduled': upcoming[0] if upcoming else None
|
||||
}
|
||||
|
||||
def create_emergency_maintenance(printer_id: int, description: str,
|
||||
priority: MaintenancePriority = MaintenancePriority.CRITICAL) -> int:
|
||||
"""Erstellt eine Notfall-Wartung"""
|
||||
task = MaintenanceTask(
|
||||
printer_id=printer_id,
|
||||
title="Notfall-Wartung",
|
||||
description=description,
|
||||
maintenance_type=MaintenanceType.EMERGENCY,
|
||||
priority=priority,
|
||||
due_date=datetime.now(), # Sofort fällig
|
||||
checklist=maintenance_manager.create_maintenance_checklist(MaintenanceType.CORRECTIVE)
|
||||
)
|
||||
|
||||
return maintenance_manager.create_task(task)
|
||||
|
||||
def schedule_preventive_maintenance(printer_id: int, interval_days: int = 30) -> MaintenanceSchedule:
|
||||
"""Plant vorbeugende Wartung"""
|
||||
return maintenance_manager.schedule_maintenance(
|
||||
printer_id=printer_id,
|
||||
maintenance_type=MaintenanceType.PREVENTIVE,
|
||||
interval_days=interval_days,
|
||||
description="Regelmäßige vorbeugende Wartung"
|
||||
)
|
||||
|
||||
# JavaScript für Wartungs-Frontend
|
||||
def get_maintenance_javascript() -> str:
|
||||
"""JavaScript für Wartungsmanagement"""
|
||||
return """
|
||||
class MaintenanceManager {
|
||||
constructor() {
|
||||
this.currentTasks = [];
|
||||
this.selectedTask = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.loadTasks();
|
||||
this.setupEventListeners();
|
||||
this.startAutoRefresh();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Task status updates
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.matches('.maintenance-status-btn')) {
|
||||
const taskId = e.target.dataset.taskId;
|
||||
const newStatus = e.target.dataset.status;
|
||||
this.updateTaskStatus(taskId, newStatus);
|
||||
}
|
||||
|
||||
if (e.target.matches('.maintenance-details-btn')) {
|
||||
const taskId = e.target.dataset.taskId;
|
||||
this.showTaskDetails(taskId);
|
||||
}
|
||||
});
|
||||
|
||||
// Create maintenance form
|
||||
const createForm = document.getElementById('create-maintenance-form');
|
||||
createForm?.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
this.createTask(new FormData(createForm));
|
||||
});
|
||||
}
|
||||
|
||||
async loadTasks() {
|
||||
try {
|
||||
const response = await fetch('/api/maintenance/tasks');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.currentTasks = data.tasks;
|
||||
this.renderTasks();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Wartungsaufgaben:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async updateTaskStatus(taskId, newStatus) {
|
||||
try {
|
||||
const response = await fetch(`/api/maintenance/tasks/${taskId}/status`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: newStatus })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.loadTasks(); // Refresh
|
||||
this.showNotification('Wartungsstatus aktualisiert', 'success');
|
||||
} else {
|
||||
this.showNotification('Fehler beim Aktualisieren', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Status-Update fehlgeschlagen:', error);
|
||||
}
|
||||
}
|
||||
|
||||
renderTasks() {
|
||||
const container = document.getElementById('maintenance-tasks-container');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = this.currentTasks.map(task => `
|
||||
<div class="maintenance-task-card ${task.status} priority-${task.priority}">
|
||||
<div class="task-header">
|
||||
<h3>${task.title}</h3>
|
||||
<span class="task-priority">${task.priority}</span>
|
||||
</div>
|
||||
<div class="task-info">
|
||||
<p><strong>Drucker:</strong> ${task.printer_id}</p>
|
||||
<p><strong>Typ:</strong> ${task.maintenance_type}</p>
|
||||
<p><strong>Fällig:</strong> ${this.formatDate(task.due_date)}</p>
|
||||
<p><strong>Status:</strong> ${task.status}</p>
|
||||
</div>
|
||||
<div class="task-actions">
|
||||
<button class="maintenance-status-btn" data-task-id="${task.id}" data-status="in_progress">
|
||||
Starten
|
||||
</button>
|
||||
<button class="maintenance-status-btn" data-task-id="${task.id}" data-status="completed">
|
||||
Abschließen
|
||||
</button>
|
||||
<button class="maintenance-details-btn" data-task-id="${task.id}">
|
||||
Details
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
showTaskDetails(taskId) {
|
||||
const task = this.currentTasks.find(t => t.id == taskId);
|
||||
if (!task) return;
|
||||
|
||||
// Create modal with task details
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'maintenance-modal';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>${task.title}</h2>
|
||||
<button class="close-modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="task-details">
|
||||
<p><strong>Beschreibung:</strong> ${task.description}</p>
|
||||
<p><strong>Techniker:</strong> ${task.assigned_technician || 'Nicht zugewiesen'}</p>
|
||||
<p><strong>Geschätzte Dauer:</strong> ${task.estimated_duration} Minuten</p>
|
||||
|
||||
${task.checklist ? this.renderChecklist(task.checklist) : ''}
|
||||
|
||||
<div class="task-notes">
|
||||
<h4>Notizen:</h4>
|
||||
<textarea id="task-notes-${taskId}" rows="4" cols="50">${task.notes || ''}</textarea>
|
||||
<button onclick="maintenanceManager.saveNotes(${taskId})">Notizen speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Close modal handlers
|
||||
modal.querySelector('.close-modal').onclick = () => modal.remove();
|
||||
modal.onclick = (e) => {
|
||||
if (e.target === modal) modal.remove();
|
||||
};
|
||||
}
|
||||
|
||||
renderChecklist(checklist) {
|
||||
return `
|
||||
<div class="maintenance-checklist">
|
||||
<h4>Checkliste:</h4>
|
||||
${checklist.map((item, index) => `
|
||||
<label class="checklist-item">
|
||||
<input type="checkbox" ${item.completed ? 'checked' : ''}
|
||||
onchange="maintenanceManager.updateChecklistItem(${index}, this.checked)">
|
||||
${item.task}
|
||||
${item.required ? '<span class="required">*</span>' : ''}
|
||||
</label>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return 'Nicht gesetzt';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('de-DE') + ' ' + date.toLocaleTimeString('de-DE', {hour: '2-digit', minute: '2-digit'});
|
||||
}
|
||||
|
||||
showNotification(message, type = 'info') {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification notification-${type}`;
|
||||
notification.textContent = message;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
startAutoRefresh() {
|
||||
setInterval(() => {
|
||||
this.loadTasks();
|
||||
}, 30000); // Refresh every 30 seconds
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
window.maintenanceManager = new MaintenanceManager();
|
||||
});
|
||||
"""
|
||||
|
||||
def create_maintenance_task(printer_id: int, title: str, description: str = "",
|
||||
maintenance_type: MaintenanceType = MaintenanceType.PREVENTIVE,
|
||||
priority: MaintenancePriority = MaintenancePriority.NORMAL) -> int:
|
||||
"""
|
||||
Erstellt eine neue Wartungsaufgabe.
|
||||
|
||||
Args:
|
||||
printer_id: ID des Druckers
|
||||
title: Titel der Wartungsaufgabe
|
||||
description: Beschreibung der Aufgabe
|
||||
maintenance_type: Art der Wartung
|
||||
priority: Priorität der Aufgabe
|
||||
|
||||
Returns:
|
||||
int: ID der erstellten Aufgabe
|
||||
"""
|
||||
task = MaintenanceTask(
|
||||
printer_id=printer_id,
|
||||
title=title,
|
||||
description=description,
|
||||
maintenance_type=maintenance_type,
|
||||
priority=priority,
|
||||
checklist=maintenance_manager.create_maintenance_checklist(maintenance_type)
|
||||
)
|
||||
|
||||
return maintenance_manager.create_task(task)
|
||||
|
||||
def schedule_maintenance(printer_id: int, maintenance_type: MaintenanceType,
|
||||
interval_days: int, description: str = "") -> MaintenanceSchedule:
|
||||
"""
|
||||
Plant regelmäßige Wartungen (Alias für maintenance_manager.schedule_maintenance).
|
||||
|
||||
Args:
|
||||
printer_id: ID des Druckers
|
||||
maintenance_type: Art der Wartung
|
||||
interval_days: Intervall in Tagen
|
||||
description: Beschreibung
|
||||
|
||||
Returns:
|
||||
MaintenanceSchedule: Erstellter Wartungsplan
|
||||
"""
|
||||
return maintenance_manager.schedule_maintenance(
|
||||
printer_id=printer_id,
|
||||
maintenance_type=maintenance_type,
|
||||
interval_days=interval_days,
|
||||
description=description
|
||||
)
|
||||
|
||||
def get_maintenance_overview() -> Dict[str, Any]:
|
||||
"""
|
||||
Holt eine Übersicht aller Wartungsaktivitäten.
|
||||
|
||||
Returns:
|
||||
Dict: Wartungsübersicht mit Statistiken und anstehenden Aufgaben
|
||||
"""
|
||||
upcoming = maintenance_manager.get_upcoming_maintenance()
|
||||
overdue = maintenance_manager.get_overdue_tasks()
|
||||
metrics = maintenance_manager.get_maintenance_metrics()
|
||||
|
||||
# Aktive Tasks
|
||||
active_tasks = [task for task in maintenance_manager.tasks.values()
|
||||
if task.status == MaintenanceStatus.IN_PROGRESS]
|
||||
|
||||
# Completed tasks in last 30 days
|
||||
thirty_days_ago = datetime.now() - timedelta(days=30)
|
||||
recent_completed = [task for task in maintenance_manager.maintenance_history
|
||||
if task.completed_at and task.completed_at >= thirty_days_ago]
|
||||
|
||||
return {
|
||||
'summary': {
|
||||
'total_tasks': len(maintenance_manager.tasks),
|
||||
'active_tasks': len(active_tasks),
|
||||
'upcoming_tasks': len(upcoming),
|
||||
'overdue_tasks': len(overdue),
|
||||
'completed_this_month': len(recent_completed)
|
||||
},
|
||||
'upcoming_tasks': [asdict(task) for task in upcoming[:10]],
|
||||
'overdue_tasks': [asdict(task) for task in overdue],
|
||||
'active_tasks': [asdict(task) for task in active_tasks],
|
||||
'recent_completed': [asdict(task) for task in recent_completed[:5]],
|
||||
'metrics': asdict(metrics),
|
||||
'schedules': {
|
||||
printer_id: [asdict(schedule) for schedule in schedules]
|
||||
for printer_id, schedules in maintenance_manager.schedules.items()
|
||||
}
|
||||
}
|
||||
|
||||
def update_maintenance_status(task_id: int, new_status: MaintenanceStatus,
|
||||
notes: str = "") -> bool:
|
||||
"""
|
||||
Aktualisiert den Status einer Wartungsaufgabe (Alias für maintenance_manager.update_task_status).
|
||||
|
||||
Args:
|
||||
task_id: ID der Wartungsaufgabe
|
||||
new_status: Neuer Status
|
||||
notes: Optionale Notizen
|
||||
|
||||
Returns:
|
||||
bool: True wenn erfolgreich aktualisiert
|
||||
"""
|
||||
return maintenance_manager.update_task_status(task_id, new_status, notes)
|
@ -1,899 +0,0 @@
|
||||
"""
|
||||
Multi-Standort-Unterstützungssystem für das MYP-System
|
||||
======================================================
|
||||
|
||||
Dieses Modul stellt umfassende Multi-Location-Funktionalität bereit:
|
||||
- Standort-Management und Hierarchien
|
||||
- Standort-spezifische Konfigurationen
|
||||
- Zentrale und dezentrale Verwaltung
|
||||
- Standort-übergreifende Berichte
|
||||
- Ressourcen-Sharing zwischen Standorten
|
||||
- Benutzer-Standort-Zuweisungen
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
from dataclasses import dataclass, asdict
|
||||
from enum import Enum
|
||||
import geocoder
|
||||
import requests
|
||||
|
||||
from utils.logging_config import get_logger
|
||||
from models import User, Printer, Job, get_db_session
|
||||
|
||||
logger = get_logger("multi_location")
|
||||
|
||||
class LocationType(Enum):
|
||||
"""Arten von Standorten"""
|
||||
HEADQUARTERS = "headquarters" # Hauptsitz
|
||||
BRANCH = "branch" # Niederlassung
|
||||
DEPARTMENT = "department" # Abteilung
|
||||
FLOOR = "floor" # Stockwerk
|
||||
ROOM = "room" # Raum
|
||||
AREA = "area" # Bereich
|
||||
|
||||
class AccessLevel(Enum):
|
||||
"""Zugriffslevel für Standorte"""
|
||||
FULL = "full" # Vollzugriff
|
||||
READ_WRITE = "read_write" # Lesen und Schreiben
|
||||
READ_ONLY = "read_only" # Nur Lesen
|
||||
NO_ACCESS = "no_access" # Kein Zugriff
|
||||
|
||||
@dataclass
|
||||
class LocationConfig:
|
||||
"""Standort-spezifische Konfiguration"""
|
||||
timezone: str = "Europe/Berlin"
|
||||
business_hours: Dict[str, str] = None
|
||||
maintenance_window: Dict[str, str] = None
|
||||
auto_approval_enabled: bool = False
|
||||
max_job_duration: int = 480 # Minuten
|
||||
contact_info: Dict[str, str] = None
|
||||
notification_settings: Dict[str, Any] = None
|
||||
|
||||
@dataclass
|
||||
class Location:
|
||||
"""Standort-Definition"""
|
||||
id: Optional[int] = None
|
||||
name: str = ""
|
||||
code: str = "" # Kurzer Code für den Standort
|
||||
location_type: LocationType = LocationType.BRANCH
|
||||
parent_id: Optional[int] = None
|
||||
address: str = ""
|
||||
city: str = ""
|
||||
country: str = ""
|
||||
postal_code: str = ""
|
||||
latitude: Optional[float] = None
|
||||
longitude: Optional[float] = None
|
||||
description: str = ""
|
||||
config: LocationConfig = None
|
||||
is_active: bool = True
|
||||
created_at: datetime = None
|
||||
manager_id: Optional[int] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.config is None:
|
||||
self.config = LocationConfig()
|
||||
if self.created_at is None:
|
||||
self.created_at = datetime.now()
|
||||
|
||||
@dataclass
|
||||
class UserLocationAccess:
|
||||
"""Benutzer-Standort-Zugriff"""
|
||||
user_id: int
|
||||
location_id: int
|
||||
access_level: AccessLevel
|
||||
granted_by: int
|
||||
granted_at: datetime
|
||||
expires_at: Optional[datetime] = None
|
||||
is_primary: bool = False
|
||||
|
||||
class MultiLocationManager:
|
||||
"""Manager für Multi-Standort-Funktionalität"""
|
||||
|
||||
def __init__(self):
|
||||
self.locations: Dict[int, Location] = {}
|
||||
self.user_access: Dict[int, List[UserLocationAccess]] = {}
|
||||
self.next_location_id = 1
|
||||
|
||||
# Standard-Standort erstellen
|
||||
self._create_default_location()
|
||||
|
||||
def _create_default_location(self):
|
||||
"""Erstellt Standard-Standort falls keiner existiert"""
|
||||
default_location = Location(
|
||||
id=1,
|
||||
name="Hauptstandort",
|
||||
code="HQ",
|
||||
location_type=LocationType.HEADQUARTERS,
|
||||
address="Mercedes-Benz Platz",
|
||||
city="Stuttgart",
|
||||
country="Deutschland",
|
||||
description="Hauptstandort des MYP-Systems"
|
||||
)
|
||||
|
||||
self.locations[1] = default_location
|
||||
self.next_location_id = 2
|
||||
|
||||
logger.info("Standard-Standort erstellt")
|
||||
|
||||
def create_location(self, location: Location) -> int:
|
||||
"""Erstellt einen neuen Standort"""
|
||||
location.id = self.next_location_id
|
||||
self.next_location_id += 1
|
||||
|
||||
# Koordinaten automatisch ermitteln
|
||||
if not location.latitude or not location.longitude:
|
||||
self._geocode_location(location)
|
||||
|
||||
self.locations[location.id] = location
|
||||
|
||||
logger.info(f"Standort erstellt: {location.name} ({location.code})")
|
||||
return location.id
|
||||
|
||||
def update_location(self, location_id: int, updates: Dict[str, Any]) -> bool:
|
||||
"""Aktualisiert einen Standort"""
|
||||
if location_id not in self.locations:
|
||||
return False
|
||||
|
||||
location = self.locations[location_id]
|
||||
|
||||
for key, value in updates.items():
|
||||
if hasattr(location, key):
|
||||
setattr(location, key, value)
|
||||
|
||||
# Koordinaten neu ermitteln bei Adressänderung
|
||||
if 'address' in updates or 'city' in updates:
|
||||
self._geocode_location(location)
|
||||
|
||||
logger.info(f"Standort aktualisiert: {location.name}")
|
||||
return True
|
||||
|
||||
def delete_location(self, location_id: int) -> bool:
|
||||
"""Löscht einen Standort (Soft Delete)"""
|
||||
if location_id not in self.locations:
|
||||
return False
|
||||
|
||||
location = self.locations[location_id]
|
||||
|
||||
# Prüfe ob Standort Kinder hat
|
||||
children = self.get_child_locations(location_id)
|
||||
if children:
|
||||
logger.warning(f"Standort {location.name} kann nicht gelöscht werden: hat Unterstandorte")
|
||||
return False
|
||||
|
||||
# Prüfe auf aktive Ressourcen
|
||||
if self._has_active_resources(location_id):
|
||||
logger.warning(f"Standort {location.name} kann nicht gelöscht werden: hat aktive Ressourcen")
|
||||
return False
|
||||
|
||||
location.is_active = False
|
||||
logger.info(f"Standort deaktiviert: {location.name}")
|
||||
return True
|
||||
|
||||
def get_location_hierarchy(self, location_id: Optional[int] = None) -> Dict[str, Any]:
|
||||
"""Holt Standort-Hierarchie"""
|
||||
if location_id:
|
||||
# Spezifische Hierarchie ab einem Standort
|
||||
location = self.locations.get(location_id)
|
||||
if not location:
|
||||
return {}
|
||||
|
||||
return self._build_hierarchy_node(location)
|
||||
else:
|
||||
# Komplette Hierarchie
|
||||
root_locations = [loc for loc in self.locations.values()
|
||||
if loc.parent_id is None and loc.is_active]
|
||||
|
||||
return {
|
||||
'locations': [self._build_hierarchy_node(loc) for loc in root_locations]
|
||||
}
|
||||
|
||||
def _build_hierarchy_node(self, location: Location) -> Dict[str, Any]:
|
||||
"""Erstellt einen Hierarchie-Knoten"""
|
||||
children = self.get_child_locations(location.id)
|
||||
|
||||
return {
|
||||
'id': location.id,
|
||||
'name': location.name,
|
||||
'code': location.code,
|
||||
'type': location.location_type.value,
|
||||
'children': [self._build_hierarchy_node(child) for child in children],
|
||||
'resource_count': self._count_location_resources(location.id)
|
||||
}
|
||||
|
||||
def get_child_locations(self, parent_id: int) -> List[Location]:
|
||||
"""Holt alle Kinder-Standorte"""
|
||||
return [loc for loc in self.locations.values()
|
||||
if loc.parent_id == parent_id and loc.is_active]
|
||||
|
||||
def get_location_path(self, location_id: int) -> List[Location]:
|
||||
"""Holt den Pfad vom Root zum Standort"""
|
||||
path = []
|
||||
current_id = location_id
|
||||
|
||||
while current_id:
|
||||
location = self.locations.get(current_id)
|
||||
if not location:
|
||||
break
|
||||
|
||||
path.insert(0, location)
|
||||
current_id = location.parent_id
|
||||
|
||||
return path
|
||||
|
||||
def grant_location_access(self, user_id: int, location_id: int,
|
||||
access_level: AccessLevel, granted_by: int,
|
||||
expires_at: Optional[datetime] = None,
|
||||
is_primary: bool = False) -> bool:
|
||||
"""Gewährt Benutzer-Zugriff auf einen Standort"""
|
||||
if location_id not in self.locations:
|
||||
return False
|
||||
|
||||
access = UserLocationAccess(
|
||||
user_id=user_id,
|
||||
location_id=location_id,
|
||||
access_level=access_level,
|
||||
granted_by=granted_by,
|
||||
granted_at=datetime.now(),
|
||||
expires_at=expires_at,
|
||||
is_primary=is_primary
|
||||
)
|
||||
|
||||
if user_id not in self.user_access:
|
||||
self.user_access[user_id] = []
|
||||
|
||||
# Entferne vorherigen Zugriff für diesen Standort
|
||||
self.user_access[user_id] = [
|
||||
acc for acc in self.user_access[user_id]
|
||||
if acc.location_id != location_id
|
||||
]
|
||||
|
||||
# Setze anderen primary-Zugriff zurück falls nötig
|
||||
if is_primary:
|
||||
for access_item in self.user_access[user_id]:
|
||||
access_item.is_primary = False
|
||||
|
||||
self.user_access[user_id].append(access)
|
||||
|
||||
logger.info(f"Standort-Zugriff gewährt: User {user_id} → Location {location_id} ({access_level.value})")
|
||||
return True
|
||||
|
||||
def revoke_location_access(self, user_id: int, location_id: int) -> bool:
|
||||
"""Entzieht Benutzer-Zugriff auf einen Standort"""
|
||||
if user_id not in self.user_access:
|
||||
return False
|
||||
|
||||
original_count = len(self.user_access[user_id])
|
||||
self.user_access[user_id] = [
|
||||
acc for acc in self.user_access[user_id]
|
||||
if acc.location_id != location_id
|
||||
]
|
||||
|
||||
success = len(self.user_access[user_id]) < original_count
|
||||
if success:
|
||||
logger.info(f"Standort-Zugriff entzogen: User {user_id} → Location {location_id}")
|
||||
|
||||
return success
|
||||
|
||||
def get_user_locations(self, user_id: int, access_level: Optional[AccessLevel] = None) -> List[Location]:
|
||||
"""Holt alle Standorte eines Benutzers"""
|
||||
if user_id not in self.user_access:
|
||||
return []
|
||||
|
||||
accessible_locations = []
|
||||
now = datetime.now()
|
||||
|
||||
for access in self.user_access[user_id]:
|
||||
# Prüfe Ablaufzeit
|
||||
if access.expires_at and access.expires_at < now:
|
||||
continue
|
||||
|
||||
# Prüfe Access Level
|
||||
if access_level and access.access_level != access_level:
|
||||
continue
|
||||
|
||||
location = self.locations.get(access.location_id)
|
||||
if location and location.is_active:
|
||||
accessible_locations.append(location)
|
||||
|
||||
return accessible_locations
|
||||
|
||||
def get_user_primary_location(self, user_id: int) -> Optional[Location]:
|
||||
"""Holt den primären Standort eines Benutzers"""
|
||||
if user_id not in self.user_access:
|
||||
return None
|
||||
|
||||
for access in self.user_access[user_id]:
|
||||
if access.is_primary:
|
||||
return self.locations.get(access.location_id)
|
||||
|
||||
# Fallback: ersten verfügbaren Standort nehmen
|
||||
user_locations = self.get_user_locations(user_id)
|
||||
return user_locations[0] if user_locations else None
|
||||
|
||||
def check_user_access(self, user_id: int, location_id: int,
|
||||
required_level: AccessLevel = AccessLevel.READ_ONLY) -> bool:
|
||||
"""Prüft ob Benutzer Zugriff auf Standort hat"""
|
||||
if user_id not in self.user_access:
|
||||
return False
|
||||
|
||||
access_levels = {
|
||||
AccessLevel.NO_ACCESS: 0,
|
||||
AccessLevel.READ_ONLY: 1,
|
||||
AccessLevel.READ_WRITE: 2,
|
||||
AccessLevel.FULL: 3
|
||||
}
|
||||
|
||||
required_level_value = access_levels[required_level]
|
||||
now = datetime.now()
|
||||
|
||||
for access in self.user_access[user_id]:
|
||||
if access.location_id != location_id:
|
||||
continue
|
||||
|
||||
# Prüfe Ablaufzeit
|
||||
if access.expires_at and access.expires_at < now:
|
||||
continue
|
||||
|
||||
user_level_value = access_levels[access.access_level]
|
||||
if user_level_value >= required_level_value:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_location_resources(self, location_id: int) -> Dict[str, Any]:
|
||||
"""Holt alle Ressourcen eines Standorts"""
|
||||
if location_id not in self.locations:
|
||||
return {}
|
||||
|
||||
# Simuliere Datenbankabfrage für Drucker und Jobs
|
||||
resources = {
|
||||
'printers': [],
|
||||
'active_jobs': [],
|
||||
'users': [],
|
||||
'pending_maintenance': 0
|
||||
}
|
||||
|
||||
# In echter Implementierung würde hier die Datenbank abgefragt
|
||||
with get_db_session() as db_session:
|
||||
# Drucker des Standorts (vereinfacht - benötigt location_id in Printer-Model)
|
||||
# printers = db_session.query(Printer).filter(Printer.location_id == location_id).all()
|
||||
# resources['printers'] = [p.to_dict() for p in printers]
|
||||
pass
|
||||
|
||||
return resources
|
||||
|
||||
def get_location_statistics(self, location_id: int,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None) -> Dict[str, Any]:
|
||||
"""Holt Statistiken für einen Standort"""
|
||||
if not start_date:
|
||||
start_date = datetime.now() - timedelta(days=30)
|
||||
if not end_date:
|
||||
end_date = datetime.now()
|
||||
|
||||
# Sammle Statistiken
|
||||
stats = {
|
||||
'location': self.locations.get(location_id, {}).name if location_id in self.locations else 'Unbekannt',
|
||||
'period': {
|
||||
'start': start_date.isoformat(),
|
||||
'end': end_date.isoformat()
|
||||
},
|
||||
'totals': {
|
||||
'printers': 0,
|
||||
'jobs_completed': 0,
|
||||
'jobs_failed': 0,
|
||||
'print_time_hours': 0,
|
||||
'material_used_kg': 0,
|
||||
'users_active': 0
|
||||
},
|
||||
'averages': {
|
||||
'jobs_per_day': 0,
|
||||
'job_duration_minutes': 0,
|
||||
'printer_utilization': 0
|
||||
},
|
||||
'trends': {
|
||||
'daily_jobs': [],
|
||||
'printer_usage': []
|
||||
}
|
||||
}
|
||||
|
||||
# In echter Implementierung würden hier Datenbankabfragen stehen
|
||||
|
||||
return stats
|
||||
|
||||
def get_multi_location_report(self, location_ids: List[int] = None) -> Dict[str, Any]:
|
||||
"""Erstellt standortübergreifenden Bericht"""
|
||||
if not location_ids:
|
||||
location_ids = list(self.locations.keys())
|
||||
|
||||
report = {
|
||||
'generated_at': datetime.now().isoformat(),
|
||||
'locations': [],
|
||||
'summary': {
|
||||
'total_locations': len(location_ids),
|
||||
'total_printers': 0,
|
||||
'total_users': 0,
|
||||
'total_jobs': 0,
|
||||
'cross_location_sharing': []
|
||||
}
|
||||
}
|
||||
|
||||
for location_id in location_ids:
|
||||
location = self.locations.get(location_id)
|
||||
if not location:
|
||||
continue
|
||||
|
||||
location_stats = self.get_location_statistics(location_id)
|
||||
location_data = {
|
||||
'id': location.id,
|
||||
'name': location.name,
|
||||
'code': location.code,
|
||||
'type': location.location_type.value,
|
||||
'statistics': location_stats
|
||||
}
|
||||
|
||||
report['locations'].append(location_data)
|
||||
|
||||
# Summiere für Gesamtübersicht
|
||||
totals = location_stats.get('totals', {})
|
||||
report['summary']['total_printers'] += totals.get('printers', 0)
|
||||
report['summary']['total_users'] += totals.get('users_active', 0)
|
||||
report['summary']['total_jobs'] += totals.get('jobs_completed', 0)
|
||||
|
||||
return report
|
||||
|
||||
def find_nearest_locations(self, latitude: float, longitude: float,
|
||||
radius_km: float = 50, limit: int = 5) -> List[Tuple[Location, float]]:
|
||||
"""Findet nächstgelegene Standorte"""
|
||||
from math import radians, sin, cos, sqrt, atan2
|
||||
|
||||
def calculate_distance(lat1, lon1, lat2, lon2):
|
||||
"""Berechnet Entfernung zwischen zwei Koordinaten (Haversine)"""
|
||||
R = 6371 # Erdradius in km
|
||||
|
||||
lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2])
|
||||
dlat = lat2 - lat1
|
||||
dlon = lon2 - lon1
|
||||
|
||||
a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
|
||||
c = 2 * atan2(sqrt(a), sqrt(1-a))
|
||||
|
||||
return R * c
|
||||
|
||||
nearby_locations = []
|
||||
|
||||
for location in self.locations.values():
|
||||
if not location.is_active or not location.latitude or not location.longitude:
|
||||
continue
|
||||
|
||||
distance = calculate_distance(
|
||||
latitude, longitude,
|
||||
location.latitude, location.longitude
|
||||
)
|
||||
|
||||
if distance <= radius_km:
|
||||
nearby_locations.append((location, distance))
|
||||
|
||||
# Sortiere nach Entfernung
|
||||
nearby_locations.sort(key=lambda x: x[1])
|
||||
|
||||
return nearby_locations[:limit]
|
||||
|
||||
def _geocode_location(self, location: Location):
|
||||
"""Ermittelt Koordinaten für einen Standort"""
|
||||
try:
|
||||
address_parts = [location.address, location.city, location.country]
|
||||
full_address = ', '.join(filter(None, address_parts))
|
||||
|
||||
if not full_address:
|
||||
return
|
||||
|
||||
# Verwende geocoder library
|
||||
result = geocoder.osm(full_address)
|
||||
|
||||
if result.ok:
|
||||
location.latitude = result.lat
|
||||
location.longitude = result.lng
|
||||
logger.info(f"Koordinaten ermittelt für {location.name}: {location.latitude}, {location.longitude}")
|
||||
else:
|
||||
logger.warning(f"Koordinaten konnten nicht ermittelt werden für {location.name}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei Geocoding für {location.name}: {str(e)}")
|
||||
|
||||
def _has_active_resources(self, location_id: int) -> bool:
|
||||
"""Prüft ob Standort aktive Ressourcen hat"""
|
||||
# Vereinfachte Implementierung
|
||||
# In echter Implementation würde hier die Datenbank geprüft
|
||||
return False
|
||||
|
||||
def _count_location_resources(self, location_id: int) -> Dict[str, int]:
|
||||
"""Zählt Ressourcen eines Standorts"""
|
||||
# Vereinfachte Implementierung
|
||||
return {
|
||||
'printers': 0,
|
||||
'users': 0,
|
||||
'jobs': 0
|
||||
}
|
||||
|
||||
# Globale Instanz
|
||||
location_manager = MultiLocationManager()
|
||||
|
||||
# Alias für Import-Kompatibilität
|
||||
LocationManager = MultiLocationManager
|
||||
|
||||
def create_location(name: str, code: str, location_type: LocationType = LocationType.BRANCH,
|
||||
address: str = "", city: str = "", country: str = "",
|
||||
parent_id: Optional[int] = None) -> int:
|
||||
"""
|
||||
Erstellt einen neuen Standort (globale Funktion).
|
||||
|
||||
Args:
|
||||
name: Name des Standorts
|
||||
code: Kurzer Code für den Standort
|
||||
location_type: Art des Standorts
|
||||
address: Adresse
|
||||
city: Stadt
|
||||
country: Land
|
||||
parent_id: Parent-Standort ID
|
||||
|
||||
Returns:
|
||||
int: ID des erstellten Standorts
|
||||
"""
|
||||
location = Location(
|
||||
name=name,
|
||||
code=code,
|
||||
location_type=location_type,
|
||||
address=address,
|
||||
city=city,
|
||||
country=country,
|
||||
parent_id=parent_id
|
||||
)
|
||||
|
||||
return location_manager.create_location(location)
|
||||
|
||||
def assign_user_to_location(user_id: int, location_id: int,
|
||||
access_level: AccessLevel = AccessLevel.READ_WRITE,
|
||||
granted_by: int = 1, is_primary: bool = False) -> bool:
|
||||
"""
|
||||
Weist einen Benutzer einem Standort zu.
|
||||
|
||||
Args:
|
||||
user_id: ID des Benutzers
|
||||
location_id: ID des Standorts
|
||||
access_level: Zugriffslevel
|
||||
granted_by: ID des gewährenden Benutzers
|
||||
is_primary: Ob dies der primäre Standort ist
|
||||
|
||||
Returns:
|
||||
bool: True wenn erfolgreich
|
||||
"""
|
||||
return location_manager.grant_location_access(
|
||||
user_id=user_id,
|
||||
location_id=location_id,
|
||||
access_level=access_level,
|
||||
granted_by=granted_by,
|
||||
is_primary=is_primary
|
||||
)
|
||||
|
||||
def get_user_locations(user_id: int) -> List[Location]:
|
||||
"""
|
||||
Holt alle Standorte eines Benutzers (globale Funktion).
|
||||
|
||||
Args:
|
||||
user_id: ID des Benutzers
|
||||
|
||||
Returns:
|
||||
List[Location]: Liste der zugänglichen Standorte
|
||||
"""
|
||||
return location_manager.get_user_locations(user_id)
|
||||
|
||||
def calculate_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
|
||||
"""
|
||||
Berechnet die Entfernung zwischen zwei Koordinaten (Haversine-Formel).
|
||||
|
||||
Args:
|
||||
lat1, lon1: Koordinaten des ersten Punkts
|
||||
lat2, lon2: Koordinaten des zweiten Punkts
|
||||
|
||||
Returns:
|
||||
float: Entfernung in Kilometern
|
||||
"""
|
||||
from math import radians, sin, cos, sqrt, atan2
|
||||
|
||||
R = 6371 # Erdradius in km
|
||||
|
||||
lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2])
|
||||
dlat = lat2 - lat1
|
||||
dlon = lon2 - lon1
|
||||
|
||||
a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
|
||||
c = 2 * atan2(sqrt(a), sqrt(1-a))
|
||||
|
||||
return R * c
|
||||
|
||||
def find_nearest_location(latitude: float, longitude: float,
|
||||
radius_km: float = 50) -> Optional[Location]:
|
||||
"""
|
||||
Findet den nächstgelegenen Standort.
|
||||
|
||||
Args:
|
||||
latitude: Breitengrad
|
||||
longitude: Längengrad
|
||||
radius_km: Suchradius in Kilometern
|
||||
|
||||
Returns:
|
||||
Optional[Location]: Nächstgelegener Standort oder None
|
||||
"""
|
||||
nearest_locations = location_manager.find_nearest_locations(
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
radius_km=radius_km,
|
||||
limit=1
|
||||
)
|
||||
|
||||
return nearest_locations[0][0] if nearest_locations else None
|
||||
|
||||
def get_location_dashboard_data(user_id: int) -> Dict[str, Any]:
|
||||
"""Holt Dashboard-Daten für Standorte eines Benutzers"""
|
||||
user_locations = location_manager.get_user_locations(user_id)
|
||||
primary_location = location_manager.get_user_primary_location(user_id)
|
||||
|
||||
dashboard_data = {
|
||||
'user_locations': [asdict(loc) for loc in user_locations],
|
||||
'primary_location': asdict(primary_location) if primary_location else None,
|
||||
'location_count': len(user_locations),
|
||||
'hierarchy': location_manager.get_location_hierarchy()
|
||||
}
|
||||
|
||||
# Füge Statistiken für jeden Standort hinzu
|
||||
for location in user_locations:
|
||||
location_stats = location_manager.get_location_statistics(location.id)
|
||||
dashboard_data[f'stats_{location.id}'] = location_stats
|
||||
|
||||
return dashboard_data
|
||||
|
||||
def create_location_from_address(name: str, address: str, city: str,
|
||||
country: str, location_type: LocationType = LocationType.BRANCH) -> int:
|
||||
"""Erstellt Standort aus Adresse mit automatischer Geocodierung"""
|
||||
location = Location(
|
||||
name=name,
|
||||
code=name[:3].upper(),
|
||||
location_type=location_type,
|
||||
address=address,
|
||||
city=city,
|
||||
country=country
|
||||
)
|
||||
|
||||
return location_manager.create_location(location)
|
||||
|
||||
# JavaScript für Multi-Location Frontend
|
||||
def get_multi_location_javascript() -> str:
|
||||
"""JavaScript für Multi-Location Management"""
|
||||
return """
|
||||
class MultiLocationManager {
|
||||
constructor() {
|
||||
this.currentLocation = null;
|
||||
this.userLocations = [];
|
||||
this.locationHierarchy = {};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.loadUserLocations();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Location switcher
|
||||
document.addEventListener('change', (e) => {
|
||||
if (e.target.matches('.location-selector')) {
|
||||
const locationId = parseInt(e.target.value);
|
||||
this.switchLocation(locationId);
|
||||
}
|
||||
});
|
||||
|
||||
// Location management buttons
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.matches('.manage-locations-btn')) {
|
||||
this.showLocationManager();
|
||||
}
|
||||
|
||||
if (e.target.matches('.location-hierarchy-btn')) {
|
||||
this.showLocationHierarchy();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async loadUserLocations() {
|
||||
try {
|
||||
const response = await fetch('/api/locations/user');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.userLocations = data.locations;
|
||||
this.currentLocation = data.primary_location;
|
||||
this.locationHierarchy = data.hierarchy;
|
||||
|
||||
this.updateLocationSelector();
|
||||
this.updateLocationDisplay();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Standorte:', error);
|
||||
}
|
||||
}
|
||||
|
||||
updateLocationSelector() {
|
||||
const selectors = document.querySelectorAll('.location-selector');
|
||||
|
||||
selectors.forEach(selector => {
|
||||
selector.innerHTML = this.userLocations.map(location =>
|
||||
`<option value="${location.id}" ${location.id === this.currentLocation?.id ? 'selected' : ''}>
|
||||
${location.name} (${location.code})
|
||||
</option>`
|
||||
).join('');
|
||||
});
|
||||
}
|
||||
|
||||
updateLocationDisplay() {
|
||||
const displays = document.querySelectorAll('.current-location-display');
|
||||
|
||||
displays.forEach(display => {
|
||||
if (this.currentLocation) {
|
||||
display.innerHTML = `
|
||||
<div class="location-info">
|
||||
<strong>${this.currentLocation.name}</strong>
|
||||
<span class="location-type">${this.currentLocation.type}</span>
|
||||
${this.currentLocation.city ? `<span class="location-city">${this.currentLocation.city}</span>` : ''}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
display.innerHTML = '<span class="no-location">Kein Standort ausgewählt</span>';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async switchLocation(locationId) {
|
||||
try {
|
||||
const response = await fetch('/api/locations/switch', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ location_id: locationId })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.currentLocation = this.userLocations.find(loc => loc.id === locationId);
|
||||
this.updateLocationDisplay();
|
||||
|
||||
// Seite neu laden um location-spezifische Daten zu aktualisieren
|
||||
window.location.reload();
|
||||
} else {
|
||||
this.showNotification('Fehler beim Wechseln des Standorts', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Standort-Wechsel fehlgeschlagen:', error);
|
||||
}
|
||||
}
|
||||
|
||||
showLocationManager() {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'location-manager-modal';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>Standort-Verwaltung</h2>
|
||||
<button class="close-modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="location-list">
|
||||
${this.renderLocationList()}
|
||||
</div>
|
||||
<div class="location-actions">
|
||||
<button class="btn-create-location">Neuen Standort erstellen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Event handlers
|
||||
modal.querySelector('.close-modal').onclick = () => modal.remove();
|
||||
modal.onclick = (e) => {
|
||||
if (e.target === modal) modal.remove();
|
||||
};
|
||||
}
|
||||
|
||||
renderLocationList() {
|
||||
return this.userLocations.map(location => `
|
||||
<div class="location-item">
|
||||
<div class="location-details">
|
||||
<h4>${location.name} (${location.code})</h4>
|
||||
<p><strong>Typ:</strong> ${location.type}</p>
|
||||
<p><strong>Adresse:</strong> ${location.address || 'Nicht angegeben'}</p>
|
||||
<p><strong>Stadt:</strong> ${location.city || 'Nicht angegeben'}</p>
|
||||
</div>
|
||||
<div class="location-actions">
|
||||
<button class="btn-edit-location" data-location-id="${location.id}">Bearbeiten</button>
|
||||
<button class="btn-view-stats" data-location-id="${location.id}">Statistiken</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
showLocationHierarchy() {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'hierarchy-modal';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>Standort-Hierarchie</h2>
|
||||
<button class="close-modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="hierarchy-tree">
|
||||
${this.renderHierarchyTree(this.locationHierarchy.locations || [])}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
modal.querySelector('.close-modal').onclick = () => modal.remove();
|
||||
modal.onclick = (e) => {
|
||||
if (e.target === modal) modal.remove();
|
||||
};
|
||||
}
|
||||
|
||||
renderHierarchyTree(locations, level = 0) {
|
||||
return locations.map(location => `
|
||||
<div class="hierarchy-node" style="margin-left: ${level * 20}px;">
|
||||
<div class="node-content">
|
||||
<span class="node-icon">${this.getLocationTypeIcon(location.type)}</span>
|
||||
<span class="node-name">${location.name}</span>
|
||||
<span class="node-code">(${location.code})</span>
|
||||
<span class="resource-count">${location.resource_count.printers || 0} Drucker</span>
|
||||
</div>
|
||||
${location.children && location.children.length > 0 ?
|
||||
this.renderHierarchyTree(location.children, level + 1) : ''}
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
getLocationTypeIcon(type) {
|
||||
const icons = {
|
||||
'headquarters': '🏢',
|
||||
'branch': '🏪',
|
||||
'department': '🏬',
|
||||
'floor': '🏢',
|
||||
'room': '🚪',
|
||||
'area': '📍'
|
||||
};
|
||||
return icons[type] || '📍';
|
||||
}
|
||||
|
||||
showNotification(message, type = 'info') {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification notification-${type}`;
|
||||
notification.textContent = message;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
window.multiLocationManager = new MultiLocationManager();
|
||||
});
|
||||
"""
|
@ -1,229 +0,0 @@
|
||||
"""
|
||||
Offline-Konfiguration für MYP-System
|
||||
===================================
|
||||
|
||||
Konfiguriert das System für den Offline-Betrieb ohne Internetverbindung.
|
||||
Stellt Fallback-Lösungen für internetabhängige Funktionen bereit.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from utils.logging_config import get_logger
|
||||
|
||||
logger = get_logger("offline_config")
|
||||
|
||||
# ===== OFFLINE-MODUS KONFIGURATION =====
|
||||
OFFLINE_MODE = True # Produktionseinstellung - System läuft offline
|
||||
|
||||
# ===== OFFLINE-KOMPATIBILITÄT PRÜFUNGEN =====
|
||||
|
||||
def check_internet_connectivity() -> bool:
|
||||
"""
|
||||
Prüft ob eine Internetverbindung verfügbar ist.
|
||||
Im Offline-Modus gibt immer False zurück.
|
||||
|
||||
Returns:
|
||||
bool: True wenn Internet verfügbar, False im Offline-Modus
|
||||
"""
|
||||
if OFFLINE_MODE:
|
||||
return False
|
||||
|
||||
# In einem echten Online-Modus könnte hier eine echte Prüfung stehen
|
||||
try:
|
||||
import socket
|
||||
socket.create_connection(("8.8.8.8", 53), timeout=3)
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
def is_oauth_available() -> bool:
|
||||
"""
|
||||
Prüft ob OAuth-Funktionalität verfügbar ist.
|
||||
|
||||
Returns:
|
||||
bool: False im Offline-Modus
|
||||
"""
|
||||
return not OFFLINE_MODE and check_internet_connectivity()
|
||||
|
||||
def is_email_sending_available() -> bool:
|
||||
"""
|
||||
Prüft ob E-Mail-Versand verfügbar ist.
|
||||
|
||||
Returns:
|
||||
bool: False im Offline-Modus (nur Logging)
|
||||
"""
|
||||
return not OFFLINE_MODE and check_internet_connectivity()
|
||||
|
||||
def is_cdn_available() -> bool:
|
||||
"""
|
||||
Prüft ob CDN-Links verfügbar sind.
|
||||
|
||||
Returns:
|
||||
bool: False im Offline-Modus (lokale Fallbacks verwenden)
|
||||
"""
|
||||
return not OFFLINE_MODE and check_internet_connectivity()
|
||||
|
||||
# ===== CDN FALLBACK-KONFIGURATION =====
|
||||
|
||||
CDN_FALLBACKS = {
|
||||
# Chart.js CDN -> Lokale Datei
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.min.js": "/static/js/charts/chart.min.js",
|
||||
"https://cdn.jsdelivr.net/npm/chart.js": "/static/js/charts/chart.min.js",
|
||||
|
||||
# FontAwesome (bereits lokal verfügbar)
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css": "/static/fontawesome/css/all.min.css",
|
||||
|
||||
# Weitere CDN-Fallbacks können hier hinzugefügt werden
|
||||
}
|
||||
|
||||
def get_local_asset_path(cdn_url: str) -> Optional[str]:
|
||||
"""
|
||||
Gibt den lokalen Pfad für eine CDN-URL zurück.
|
||||
|
||||
Args:
|
||||
cdn_url: URL des CDN-Assets
|
||||
|
||||
Returns:
|
||||
str: Lokaler Pfad oder None wenn kein Fallback verfügbar
|
||||
"""
|
||||
return CDN_FALLBACKS.get(cdn_url)
|
||||
|
||||
def replace_cdn_links(html_content: str) -> str:
|
||||
"""
|
||||
Ersetzt CDN-Links durch lokale Fallbacks im HTML-Inhalt.
|
||||
|
||||
Args:
|
||||
html_content: HTML-Inhalt mit CDN-Links
|
||||
|
||||
Returns:
|
||||
str: HTML-Inhalt mit lokalen Links
|
||||
"""
|
||||
if not OFFLINE_MODE:
|
||||
return html_content
|
||||
|
||||
modified_content = html_content
|
||||
|
||||
for cdn_url, local_path in CDN_FALLBACKS.items():
|
||||
if cdn_url in modified_content:
|
||||
modified_content = modified_content.replace(cdn_url, local_path)
|
||||
logger.info(f"🔄 CDN-Link ersetzt: {cdn_url} -> {local_path}")
|
||||
|
||||
return modified_content
|
||||
|
||||
# ===== SECURITY POLICY ANPASSUNGEN =====
|
||||
|
||||
def get_offline_csp_policy() -> Dict[str, List[str]]:
|
||||
"""
|
||||
Gibt CSP-Policy für Offline-Modus zurück.
|
||||
Entfernt externe CDN-Domains aus der Policy.
|
||||
|
||||
Returns:
|
||||
Dict: CSP-Policy ohne externe Domains
|
||||
"""
|
||||
if not OFFLINE_MODE:
|
||||
# Online-Modus: Originale Policy mit CDNs
|
||||
return {
|
||||
"script-src": [
|
||||
"'self'",
|
||||
"'unsafe-inline'",
|
||||
"'unsafe-eval'",
|
||||
"https://cdn.jsdelivr.net",
|
||||
"https://unpkg.com",
|
||||
"https://cdnjs.cloudflare.com"
|
||||
],
|
||||
"style-src": [
|
||||
"'self'",
|
||||
"'unsafe-inline'",
|
||||
"https://fonts.googleapis.com",
|
||||
"https://cdn.jsdelivr.net"
|
||||
],
|
||||
"font-src": [
|
||||
"'self'",
|
||||
"https://fonts.gstatic.com"
|
||||
]
|
||||
}
|
||||
else:
|
||||
# Offline-Modus: Nur lokale Ressourcen
|
||||
return {
|
||||
"script-src": [
|
||||
"'self'",
|
||||
"'unsafe-inline'",
|
||||
"'unsafe-eval'"
|
||||
],
|
||||
"style-src": [
|
||||
"'self'",
|
||||
"'unsafe-inline'"
|
||||
],
|
||||
"font-src": [
|
||||
"'self'"
|
||||
],
|
||||
"img-src": [
|
||||
"'self'",
|
||||
"data:"
|
||||
]
|
||||
}
|
||||
|
||||
# ===== OFFLINE-MODUS HILFSFUNKTIONEN =====
|
||||
|
||||
def log_offline_mode_status():
|
||||
"""Loggt den aktuellen Offline-Modus Status."""
|
||||
if OFFLINE_MODE:
|
||||
logger.info("🌐 System läuft im OFFLINE-MODUS")
|
||||
logger.info(" ❌ OAuth deaktiviert")
|
||||
logger.info(" ❌ E-Mail-Versand deaktiviert (nur Logging)")
|
||||
logger.info(" ❌ CDN-Links werden durch lokale Dateien ersetzt")
|
||||
logger.info(" ✅ Alle Kernfunktionen verfügbar")
|
||||
else:
|
||||
logger.info("🌐 System läuft im ONLINE-MODUS")
|
||||
logger.info(" ✅ OAuth verfügbar")
|
||||
logger.info(" ✅ E-Mail-Versand verfügbar")
|
||||
logger.info(" ✅ CDN-Links verfügbar")
|
||||
|
||||
def get_feature_availability() -> Dict[str, bool]:
|
||||
"""
|
||||
Gibt die Verfügbarkeit verschiedener Features zurück.
|
||||
|
||||
Returns:
|
||||
Dict: Feature-Verfügbarkeit
|
||||
"""
|
||||
return {
|
||||
"oauth": is_oauth_available(),
|
||||
"email_sending": is_email_sending_available(),
|
||||
"cdn_resources": is_cdn_available(),
|
||||
"offline_mode": OFFLINE_MODE,
|
||||
"core_functionality": True, # Kernfunktionen immer verfügbar
|
||||
"printer_control": True, # Drucker-Steuerung immer verfügbar
|
||||
"job_management": True, # Job-Verwaltung immer verfügbar
|
||||
"user_management": True # Benutzer-Verwaltung immer verfügbar
|
||||
}
|
||||
|
||||
# ===== STARTUP-FUNKTIONEN =====
|
||||
|
||||
def initialize_offline_mode():
|
||||
"""Initialisiert den Offline-Modus beim System-Start."""
|
||||
log_offline_mode_status()
|
||||
|
||||
if OFFLINE_MODE:
|
||||
logger.info("🔧 Initialisiere Offline-Modus-Anpassungen...")
|
||||
|
||||
# Prüfe ob lokale Chart.js verfügbar ist
|
||||
chart_js_path = "static/js/charts/chart.min.js"
|
||||
if not os.path.exists(chart_js_path):
|
||||
logger.warning(f"⚠️ Lokale Chart.js nicht gefunden: {chart_js_path}")
|
||||
logger.warning(" Diagramme könnten nicht funktionieren")
|
||||
else:
|
||||
logger.info(f"✅ Lokale Chart.js gefunden: {chart_js_path}")
|
||||
|
||||
# Prüfe weitere lokale Assets
|
||||
fontawesome_path = "static/fontawesome/css/all.min.css"
|
||||
if not os.path.exists(fontawesome_path):
|
||||
logger.warning(f"⚠️ Lokale FontAwesome nicht gefunden: {fontawesome_path}")
|
||||
else:
|
||||
logger.info(f"✅ Lokale FontAwesome gefunden: {fontawesome_path}")
|
||||
|
||||
logger.info("✅ Offline-Modus erfolgreich initialisiert")
|
||||
|
||||
# Beim Import automatisch initialisieren
|
||||
initialize_offline_mode()
|
@ -1,243 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Test-Skript für Button-Funktionalitäten
|
||||
Testet alle Buttons aus dem Selenium-Test auf echte Funktionalität
|
||||
"""
|
||||
|
||||
import requests
|
||||
import time
|
||||
import json
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
class ButtonFunctionalityTester:
|
||||
def __init__(self, base_url="http://127.0.0.1:5000"):
|
||||
self.base_url = base_url
|
||||
self.session = requests.Session()
|
||||
self.driver = None
|
||||
|
||||
def setup_driver(self):
|
||||
"""Selenium WebDriver einrichten"""
|
||||
try:
|
||||
self.driver = webdriver.Chrome()
|
||||
self.driver.set_window_size(1696, 1066)
|
||||
print("✅ WebDriver erfolgreich initialisiert")
|
||||
except Exception as e:
|
||||
print(f"❌ Fehler beim Initialisieren des WebDrivers: {e}")
|
||||
|
||||
def login(self, username="admin", password="admin"):
|
||||
"""Anmeldung durchführen"""
|
||||
try:
|
||||
self.driver.get(f"{self.base_url}/auth/login")
|
||||
|
||||
# Warten bis Login-Formular geladen ist
|
||||
username_field = WebDriverWait(self.driver, 10).until(
|
||||
EC.presence_of_element_located((By.NAME, "username"))
|
||||
)
|
||||
|
||||
username_field.send_keys(username)
|
||||
self.driver.find_element(By.NAME, "password").send_keys(password)
|
||||
self.driver.find_element(By.XPATH, "//button[@type='submit']").click()
|
||||
|
||||
# Warten bis Dashboard geladen ist
|
||||
WebDriverWait(self.driver, 10).until(
|
||||
EC.url_contains("/dashboard")
|
||||
)
|
||||
|
||||
print("✅ Erfolgreich angemeldet")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Fehler bei der Anmeldung: {e}")
|
||||
return False
|
||||
|
||||
def test_button_functionality(self, button_selector, button_name, expected_action=""):
|
||||
"""Teste einen einzelnen Button auf Funktionalität"""
|
||||
try:
|
||||
print(f"\n🔍 Teste Button: {button_name} ({button_selector})")
|
||||
|
||||
# Button finden
|
||||
button = WebDriverWait(self.driver, 5).until(
|
||||
EC.element_to_be_clickable((By.CSS_SELECTOR, button_selector))
|
||||
)
|
||||
|
||||
# Ursprünglichen Zustand erfassen
|
||||
original_url = self.driver.current_url
|
||||
original_text = button.text if button.text else "Kein Text"
|
||||
|
||||
print(f" 📍 Button gefunden: '{original_text}'")
|
||||
|
||||
# Button klicken
|
||||
button.click()
|
||||
print(f" 👆 Button geklickt")
|
||||
|
||||
# Kurz warten für Reaktion
|
||||
time.sleep(1)
|
||||
|
||||
# Reaktion prüfen
|
||||
reactions = []
|
||||
|
||||
# URL-Änderung prüfen
|
||||
if self.driver.current_url != original_url:
|
||||
reactions.append(f"URL-Änderung: {self.driver.current_url}")
|
||||
|
||||
# Modal-Fenster prüfen
|
||||
try:
|
||||
modal = self.driver.find_element(By.CSS_SELECTOR, ".fixed.inset-0")
|
||||
if modal.is_displayed():
|
||||
reactions.append("Modal-Fenster geöffnet")
|
||||
except:
|
||||
pass
|
||||
|
||||
# Loading-Spinner prüfen
|
||||
try:
|
||||
spinner = self.driver.find_element(By.CSS_SELECTOR, ".animate-spin")
|
||||
if spinner.is_displayed():
|
||||
reactions.append("Loading-Animation aktiv")
|
||||
except:
|
||||
pass
|
||||
|
||||
# Toast-Benachrichtigung prüfen
|
||||
try:
|
||||
toast = self.driver.find_element(By.CSS_SELECTOR, ".fixed.top-4.right-4")
|
||||
if toast.is_displayed():
|
||||
reactions.append(f"Toast-Nachricht: {toast.text}")
|
||||
except:
|
||||
pass
|
||||
|
||||
# Button-Text-Änderung prüfen
|
||||
new_text = button.text if button.text else "Kein Text"
|
||||
if new_text != original_text:
|
||||
reactions.append(f"Text-Änderung: '{original_text}' → '{new_text}'")
|
||||
|
||||
# Ergebnis ausgeben
|
||||
if reactions:
|
||||
print(f" ✅ Reaktionen gefunden:")
|
||||
for reaction in reactions:
|
||||
print(f" - {reaction}")
|
||||
return True
|
||||
else:
|
||||
print(f" ⚠️ Keine sichtbare Reaktion erkannt")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Fehler beim Testen: {e}")
|
||||
return False
|
||||
|
||||
def test_all_buttons(self):
|
||||
"""Teste alle Buttons aus dem Selenium-Test"""
|
||||
if not self.setup_driver():
|
||||
return
|
||||
|
||||
if not self.login():
|
||||
return
|
||||
|
||||
# Button-Test-Plan basierend auf Selenium-Test
|
||||
button_tests = [
|
||||
# Dashboard-Seite (Startseite)
|
||||
{
|
||||
"page": f"{self.base_url}/",
|
||||
"buttons": [
|
||||
(".mb-8 > .btn-primary", "Haupt-CTA Button"),
|
||||
(".btn-primary > span", "CTA Button Span")
|
||||
]
|
||||
},
|
||||
|
||||
# Dashboard-Seite
|
||||
{
|
||||
"page": f"{self.base_url}/dashboard",
|
||||
"buttons": [
|
||||
("#refreshDashboard > span", "Dashboard Aktualisieren")
|
||||
]
|
||||
},
|
||||
|
||||
# Drucker-Seite
|
||||
{
|
||||
"page": f"{self.base_url}/printers",
|
||||
"buttons": [
|
||||
("#refresh-button > span", "Drucker Aktualisieren"),
|
||||
("#maintenance-toggle > span", "Wartungsmodus Toggle")
|
||||
]
|
||||
},
|
||||
|
||||
# Jobs-Seite
|
||||
{
|
||||
"page": f"{self.base_url}/jobs",
|
||||
"buttons": [
|
||||
("#batch-toggle > span", "Mehrfachauswahl Toggle"),
|
||||
("#refresh-button > span", "Jobs Aktualisieren")
|
||||
]
|
||||
},
|
||||
|
||||
# Admin-Seite
|
||||
{
|
||||
"page": f"{self.base_url}/admin",
|
||||
"buttons": [
|
||||
("#analytics-btn", "Analytics Button"),
|
||||
("#maintenance-btn", "Wartung Button"),
|
||||
("#system-status-btn", "System Status Button"),
|
||||
("#add-user-btn", "Benutzer hinzufügen")
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
results = {"total": 0, "working": 0, "broken": 0}
|
||||
|
||||
print("🚀 Starte umfassenden Button-Funktionalitäts-Test...\n")
|
||||
|
||||
for test_group in button_tests:
|
||||
print(f"📄 Navigiere zu Seite: {test_group['page']}")
|
||||
|
||||
try:
|
||||
self.driver.get(test_group["page"])
|
||||
time.sleep(2) # Seite laden lassen
|
||||
|
||||
for selector, name in test_group["buttons"]:
|
||||
results["total"] += 1
|
||||
if self.test_button_functionality(selector, name):
|
||||
results["working"] += 1
|
||||
else:
|
||||
results["broken"] += 1
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Fehler beim Laden der Seite {test_group['page']}: {e}")
|
||||
|
||||
# Zusammenfassung
|
||||
print(f"\n{'='*60}")
|
||||
print(f"📊 TEST-ZUSAMMENFASSUNG")
|
||||
print(f"{'='*60}")
|
||||
print(f"Getestete Buttons gesamt: {results['total']}")
|
||||
print(f"✅ Funktional: {results['working']}")
|
||||
print(f"❌ Nicht funktional: {results['broken']}")
|
||||
|
||||
success_rate = (results['working'] / results['total']) * 100 if results['total'] > 0 else 0
|
||||
print(f"📈 Erfolgsrate: {success_rate:.1f}%")
|
||||
|
||||
if success_rate >= 90:
|
||||
print("🎉 AUSGEZEICHNET! Fast alle Buttons funktionieren korrekt.")
|
||||
elif success_rate >= 75:
|
||||
print("✅ GUT! Die meisten Buttons funktionieren korrekt.")
|
||||
elif success_rate >= 50:
|
||||
print("⚠️ BEFRIEDIGEND! Einige Buttons benötigen noch Verbesserungen.")
|
||||
else:
|
||||
print("❌ VERBESSERUNG ERFORDERLICH! Viele Buttons haben keine Funktionalität.")
|
||||
|
||||
def cleanup(self):
|
||||
"""Aufräumen"""
|
||||
if self.driver:
|
||||
self.driver.quit()
|
||||
print("🧹 WebDriver beendet")
|
||||
|
||||
def main():
|
||||
"""Hauptfunktion"""
|
||||
tester = ButtonFunctionalityTester()
|
||||
|
||||
try:
|
||||
tester.test_all_buttons()
|
||||
finally:
|
||||
tester.cleanup()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -1,175 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
P110-TAPO-TEST - Speziell für TP-Link Tapo P110-Steckdosen
|
||||
Testet verschiedene Versionen des PyP100-Moduls
|
||||
"""
|
||||
|
||||
import sys
|
||||
import time
|
||||
import socket
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
|
||||
# Anmeldedaten
|
||||
TAPO_USERNAME = "till.tomczak@mercedes-benz.com"
|
||||
TAPO_PASSWORD = "744563017196"
|
||||
|
||||
# Standard-IP-Adressen zum Testen (anpassen an tatsächliche IPs)
|
||||
TEST_IPS = [
|
||||
"192.168.0.103", # Diese IPs waren erreichbar im vorherigen Test
|
||||
"192.168.0.104"
|
||||
]
|
||||
|
||||
def log(message):
|
||||
"""Logge eine Nachricht mit Zeitstempel"""
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
print(f"[{timestamp}] {message}")
|
||||
|
||||
def check_connection(ip, port=80, timeout=1):
|
||||
"""Prüft eine TCP-Verbindung"""
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(timeout)
|
||||
result = sock.connect_ex((ip, port))
|
||||
sock.close()
|
||||
return result == 0
|
||||
except:
|
||||
return False
|
||||
|
||||
def install_package(package):
|
||||
"""Installiert ein Python-Paket"""
|
||||
try:
|
||||
log(f"Installiere {package}...")
|
||||
subprocess.run([sys.executable, "-m", "pip", "install", package, "--force-reinstall"], check=True)
|
||||
log(f"✅ {package} erfolgreich installiert")
|
||||
return True
|
||||
except Exception as e:
|
||||
log(f"❌ Fehler bei Installation von {package}: {e}")
|
||||
return False
|
||||
|
||||
def test_p110_connection():
|
||||
"""Testet verschiedene Möglichkeiten, um mit P110-Steckdosen zu kommunizieren"""
|
||||
|
||||
log("🚀 TAPO P110 TEST - STARTER")
|
||||
log(f"👤 Benutzername: {TAPO_USERNAME}")
|
||||
log(f"🔑 Passwort: {TAPO_PASSWORD}")
|
||||
|
||||
# Verfügbare Module testen
|
||||
log("\n1️⃣ SCHRITT 1: Teste verfügbare Module")
|
||||
|
||||
try:
|
||||
from PyP100 import PyP110
|
||||
log("✅ PyP100 Modul gefunden")
|
||||
except ImportError:
|
||||
log("❌ PyP100 Modul nicht gefunden")
|
||||
install_package("PyP100==0.1.2")
|
||||
|
||||
try:
|
||||
from PyP100 import PyP110
|
||||
log("✅ PyP100 Modul jetzt installiert")
|
||||
except ImportError:
|
||||
log("❌ Konnte PyP100 nicht importieren")
|
||||
return
|
||||
|
||||
# Erreichbare Steckdosen finden
|
||||
log("\n2️⃣ SCHRITT 2: Suche erreichbare IPs")
|
||||
|
||||
available_ips = []
|
||||
for ip in TEST_IPS:
|
||||
if check_connection(ip):
|
||||
log(f"✅ IP {ip} ist erreichbar")
|
||||
available_ips.append(ip)
|
||||
else:
|
||||
log(f"❌ IP {ip} nicht erreichbar")
|
||||
|
||||
if not available_ips:
|
||||
log("❌ Keine erreichbaren IPs gefunden!")
|
||||
return
|
||||
|
||||
# P110-Verbindung testen
|
||||
log("\n3️⃣ SCHRITT 3: Teste PyP100 Bibliothek")
|
||||
|
||||
for ip in available_ips:
|
||||
try:
|
||||
log(f"🔄 Verbinde zu Steckdose {ip} mit PyP100.PyP110...")
|
||||
|
||||
# Neue Instanz erstellen
|
||||
from PyP100 import PyP110
|
||||
p110 = PyP110.P110(ip, TAPO_USERNAME, TAPO_PASSWORD)
|
||||
|
||||
# Handshake und Login
|
||||
log(" Handshake...")
|
||||
p110.handshake()
|
||||
log(" Login...")
|
||||
p110.login()
|
||||
|
||||
# Geräteinformationen abrufen
|
||||
log(" Geräteinformationen abrufen...")
|
||||
device_info = p110.getDeviceInfo()
|
||||
|
||||
# Erfolg!
|
||||
log(f"✅ ERFOLG! Steckdose {ip} gefunden")
|
||||
log(f" Name: {device_info.get('nickname', 'Unbekannt')}")
|
||||
log(f" Status: {'Eingeschaltet' if device_info.get('device_on', False) else 'Ausgeschaltet'}")
|
||||
|
||||
# Ein-/Ausschalten testen
|
||||
if "--toggle" in sys.argv:
|
||||
current_state = device_info.get('device_on', False)
|
||||
|
||||
if current_state:
|
||||
log(" Schalte AUS...")
|
||||
p110.turnOff()
|
||||
else:
|
||||
log(" Schalte EIN...")
|
||||
p110.turnOn()
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
# Status prüfen
|
||||
device_info = p110.getDeviceInfo()
|
||||
new_state = device_info.get('device_on', False)
|
||||
log(f" Neuer Status: {'Eingeschaltet' if new_state else 'Ausgeschaltet'}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
log(f"❌ Fehler bei Verbindung zu {ip}: {e}")
|
||||
|
||||
# Alternative Bibliothek testen
|
||||
log("\n4️⃣ SCHRITT 4: Teste PyP100 mit alternativer Version")
|
||||
|
||||
if install_package("pytapo==1.1.2"):
|
||||
try:
|
||||
import pytapo
|
||||
from pytapo.tapo import Tapo
|
||||
|
||||
for ip in available_ips:
|
||||
try:
|
||||
log(f"🔄 Verbinde zu Steckdose {ip} mit pytapo...")
|
||||
|
||||
# Neue Verbindung
|
||||
tapo = Tapo(ip, TAPO_USERNAME, TAPO_PASSWORD)
|
||||
|
||||
# Geräteinformationen abrufen
|
||||
device_info = tapo.get_device_info()
|
||||
|
||||
# Erfolg!
|
||||
log(f"✅ ERFOLG mit pytapo! Steckdose {ip} gefunden")
|
||||
log(f" Name: {device_info.get('nickname', 'Unbekannt')}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
log(f"❌ Fehler bei pytapo-Verbindung zu {ip}: {e}")
|
||||
except Exception as e:
|
||||
log(f"❌ Fehler beim Import von pytapo: {e}")
|
||||
|
||||
log("\n❌ Keine funktionierenden Tapo-Steckdosen gefunden!")
|
||||
log("Bitte überprüfen Sie die Anmeldedaten und IP-Adressen")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("\n======= TAPO P110 TEST =======\n")
|
||||
test_p110_connection()
|
||||
print("\n======= TEST BEENDET =======\n")
|
@ -1,212 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
DIREKT-TEST für TP-Link Tapo P110-Steckdosen
|
||||
Umgeht Ping-Befehle und testet direkte TCP-Verbindung
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import socket
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
# Anmeldedaten für Tapo-Steckdosen
|
||||
TAPO_USERNAME = "till.tomczak@mercedes-benz.com"
|
||||
TAPO_PASSWORD = "744563017196A"
|
||||
|
||||
# Standard-IPs für Tapo-Steckdosen
|
||||
# (falls nicht verfügbar, passen Sie diese an die tatsächlichen IPs in Ihrem Netzwerk an)
|
||||
TAPO_IPS = [
|
||||
# Typische IP-Bereiche
|
||||
"192.168.1.100",
|
||||
"192.168.1.101",
|
||||
"192.168.1.102",
|
||||
"192.168.1.103",
|
||||
"192.168.1.104",
|
||||
"192.168.1.105",
|
||||
"192.168.0.100",
|
||||
"192.168.0.101",
|
||||
"192.168.0.102",
|
||||
"192.168.0.103",
|
||||
"192.168.0.104",
|
||||
"192.168.0.105",
|
||||
|
||||
# Mercedes-Benz Netzwerk spezifisch
|
||||
"10.0.0.100",
|
||||
"10.0.0.101",
|
||||
"10.0.0.102",
|
||||
"10.0.0.103",
|
||||
"10.0.0.104",
|
||||
"10.0.0.105",
|
||||
|
||||
# Zusätzliche mögliche IPs
|
||||
"192.168.178.100",
|
||||
"192.168.178.101",
|
||||
"192.168.178.102",
|
||||
"192.168.178.103",
|
||||
"192.168.178.104",
|
||||
"192.168.178.105",
|
||||
]
|
||||
|
||||
def log_message(message, level="INFO"):
|
||||
"""Logge eine Nachricht mit Zeitstempel"""
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
print(f"[{timestamp}] [{level}] {message}")
|
||||
|
||||
def check_tcp_connection(host, port=80, timeout=1):
|
||||
"""
|
||||
Prüft ob eine TCP-Verbindung zu einem Host und Port möglich ist.
|
||||
Vermeidet Ping und charmap-Probleme.
|
||||
|
||||
Args:
|
||||
host: Hostname oder IP-Adresse
|
||||
port: TCP-Port (Standard: 80)
|
||||
timeout: Timeout in Sekunden
|
||||
|
||||
Returns:
|
||||
bool: True wenn Verbindung erfolgreich
|
||||
"""
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(timeout)
|
||||
result = sock.connect_ex((host, port))
|
||||
sock.close()
|
||||
return result == 0
|
||||
except:
|
||||
return False
|
||||
|
||||
def test_tapo_connection():
|
||||
"""
|
||||
Testet die Verbindung zu TP-Link Tapo P110-Steckdosen.
|
||||
"""
|
||||
log_message("🔄 Überprüfe ob PyP100-Modul installiert ist...")
|
||||
|
||||
try:
|
||||
from PyP100 import PyP110
|
||||
log_message("✅ PyP100-Modul erfolgreich importiert")
|
||||
except ImportError:
|
||||
log_message("❌ PyP100-Modul nicht installiert", "ERROR")
|
||||
log_message(" Installiere jetzt mit: pip install PyP100==0.1.2")
|
||||
|
||||
try:
|
||||
import subprocess
|
||||
subprocess.run([sys.executable, "-m", "pip", "install", "PyP100==0.1.2"], check=True)
|
||||
log_message("✅ PyP100-Modul erfolgreich installiert")
|
||||
|
||||
# Erneut importieren
|
||||
from PyP100 import PyP110
|
||||
except Exception as e:
|
||||
log_message(f"❌ Fehler bei Installation von PyP100: {str(e)}", "ERROR")
|
||||
log_message(" Bitte installieren Sie manuell: pip install PyP100==0.1.2")
|
||||
return
|
||||
|
||||
log_message("🔍 Starte Test für Tapo-Steckdosen...")
|
||||
log_message(f"🔐 Anmeldedaten: {TAPO_USERNAME} / {TAPO_PASSWORD}")
|
||||
|
||||
successful_connections = 0
|
||||
found_ips = []
|
||||
|
||||
for ip in TAPO_IPS:
|
||||
log_message(f"🔄 Teste IP-Adresse: {ip}")
|
||||
|
||||
# TCP-Verbindungstest für Grundkonnektivität (Port 80 für HTTP)
|
||||
conn_success = check_tcp_connection(ip, port=80)
|
||||
|
||||
if not conn_success:
|
||||
# Alternativ Port 443 testen für HTTPS
|
||||
conn_success = check_tcp_connection(ip, port=443)
|
||||
|
||||
if not conn_success:
|
||||
log_message(f" ❌ IP {ip} nicht erreichbar (Verbindung fehlgeschlagen)")
|
||||
continue
|
||||
|
||||
log_message(f" ✅ IP {ip} ist erreichbar (TCP-Verbindung erfolgreich)")
|
||||
|
||||
# Tapo-Verbindung testen
|
||||
try:
|
||||
log_message(f" 🔄 Verbinde zu Tapo-Steckdose {ip}...")
|
||||
p110 = PyP110.P110(ip, TAPO_USERNAME, TAPO_PASSWORD)
|
||||
p110.handshake() # Authentifizierung
|
||||
p110.login() # Login
|
||||
|
||||
# Geräteinformationen abrufen
|
||||
device_info = p110.getDeviceInfo()
|
||||
|
||||
# Status abrufen
|
||||
is_on = device_info.get('device_on', False)
|
||||
nickname = device_info.get('nickname', "Unbekannt")
|
||||
|
||||
log_message(f" ✅ Verbindung zu Tapo-Steckdose '{nickname}' ({ip}) erfolgreich")
|
||||
log_message(f" 📱 Gerätename: {nickname}")
|
||||
log_message(f" ⚡ Status: {'Eingeschaltet' if is_on else 'Ausgeschaltet'}")
|
||||
|
||||
if 'on_time' in device_info:
|
||||
on_time = device_info.get('on_time', 0)
|
||||
hours, minutes = divmod(on_time // 60, 60)
|
||||
log_message(f" ⏱️ Betriebszeit: {hours}h {minutes}m")
|
||||
|
||||
successful_connections += 1
|
||||
found_ips.append(ip)
|
||||
|
||||
# Steckdose testen: EIN/AUS
|
||||
if len(sys.argv) > 1 and sys.argv[1] == '--toggle':
|
||||
if is_on:
|
||||
log_message(f" 🔄 Schalte Steckdose {nickname} AUS...")
|
||||
p110.turnOff()
|
||||
log_message(f" ✅ Steckdose ausgeschaltet")
|
||||
else:
|
||||
log_message(f" 🔄 Schalte Steckdose {nickname} EIN...")
|
||||
p110.turnOn()
|
||||
log_message(f" ✅ Steckdose eingeschaltet")
|
||||
|
||||
# Kurze Pause
|
||||
time.sleep(1)
|
||||
|
||||
# Status erneut abrufen
|
||||
device_info = p110.getDeviceInfo()
|
||||
is_on = device_info.get('device_on', False)
|
||||
log_message(f" ⚡ Neuer Status: {'Eingeschaltet' if is_on else 'Ausgeschaltet'}")
|
||||
|
||||
except Exception as e:
|
||||
log_message(f" ❌ Verbindung zu Tapo-Steckdose {ip} fehlgeschlagen: {str(e)}", "ERROR")
|
||||
|
||||
# Zusammenfassung
|
||||
log_message("\n📊 Zusammenfassung:")
|
||||
log_message(f" Getestete IPs: {len(TAPO_IPS)}")
|
||||
log_message(f" Gefundene Tapo-Steckdosen: {successful_connections}")
|
||||
|
||||
if successful_connections > 0:
|
||||
log_message("✅ Tapo-Steckdosen erfolgreich gefunden und getestet!")
|
||||
log_message(f"📝 Gefundene IPs: {found_ips}")
|
||||
|
||||
# Ausgabe für Konfiguration
|
||||
log_message("\n🔧 Konfiguration für settings.py:")
|
||||
log_message(f"""
|
||||
# TP-Link Tapo Standard-Anmeldedaten
|
||||
TAPO_USERNAME = "{TAPO_USERNAME}"
|
||||
TAPO_PASSWORD = "{TAPO_PASSWORD}"
|
||||
|
||||
# Automatische Steckdosen-Erkennung aktivieren
|
||||
TAPO_AUTO_DISCOVERY = True
|
||||
|
||||
# Standard-Steckdosen-IPs
|
||||
DEFAULT_TAPO_IPS = {found_ips}
|
||||
""")
|
||||
else:
|
||||
log_message("❌ Keine Tapo-Steckdosen gefunden!", "ERROR")
|
||||
log_message(" Bitte überprüfen Sie die IP-Adressen und Anmeldedaten")
|
||||
|
||||
# Fehlerbehebungs-Tipps
|
||||
log_message("\n🔧 Fehlerbehebungs-Tipps:")
|
||||
log_message("1. Stellen Sie sicher, dass die Steckdosen mit dem WLAN verbunden sind")
|
||||
log_message("2. Prüfen Sie die IP-Adressen in der Tapo-App oder im Router")
|
||||
log_message("3. Stellen Sie sicher, dass die Anmeldedaten korrekt sind")
|
||||
log_message("4. Prüfen Sie ob die Steckdosen über die Tapo-App erreichbar sind")
|
||||
log_message("5. Führen Sie einen Neustart der Steckdosen durch (aus- und wieder einstecken)")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("\n====== TAPO P110 DIREKT-TEST (OHNE PING) ======\n")
|
||||
test_tapo_connection()
|
||||
print("\n====== TEST ABGESCHLOSSEN ======\n")
|
@ -1,132 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
SOFORT-TEST für TP-Link Tapo P110-Steckdosen
|
||||
Nutzt direkt PyP100 mit hardkodierten Anmeldedaten
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
# TAPO Anmeldedaten direkt hardkodiert (wie in den funktionierenden Versionen)
|
||||
os.environ["TAPO_USERNAME"] = "till.tomczak@mercedes-benz.com"
|
||||
os.environ["TAPO_PASSWORD"] = "744563017196A" # Das 'A' am Ende ist wichtig
|
||||
|
||||
# IPs der Steckdosen
|
||||
TAPO_IPS = [
|
||||
"192.168.0.103",
|
||||
"192.168.0.104",
|
||||
"192.168.0.100",
|
||||
"192.168.0.101",
|
||||
"192.168.0.102"
|
||||
]
|
||||
|
||||
def log(msg):
|
||||
"""Protokolliert eine Nachricht mit Zeitstempel"""
|
||||
timestamp = datetime.now().strftime("%H:%M:%S")
|
||||
print(f"[{timestamp}] {msg}")
|
||||
|
||||
def test_connection():
|
||||
"""Teste Verbindung zu den Steckdosen"""
|
||||
log("🔄 Teste PyP100-Import...")
|
||||
|
||||
try:
|
||||
from PyP100 import PyP100
|
||||
log("✅ PyP100-Modul erfolgreich importiert")
|
||||
except ImportError:
|
||||
log("❌ PyP100-Modul nicht gefunden. Installiere es...")
|
||||
try:
|
||||
import subprocess
|
||||
subprocess.run([sys.executable, "-m", "pip", "install", "PyP100==0.0.12"], check=True)
|
||||
from PyP100 import PyP100
|
||||
log("✅ PyP100-Modul installiert")
|
||||
except Exception as e:
|
||||
log(f"❌ Fehler bei Installation: {str(e)}")
|
||||
return False
|
||||
|
||||
# Anmeldedaten aus Umgebungsvariablen lesen
|
||||
username = os.environ.get("TAPO_USERNAME")
|
||||
password = os.environ.get("TAPO_PASSWORD")
|
||||
|
||||
log(f"👤 Benutzername: {username}")
|
||||
log(f"🔑 Passwort: {password}")
|
||||
|
||||
# Teste jede IP
|
||||
success = False
|
||||
|
||||
for ip in TAPO_IPS:
|
||||
log(f"🔄 Teste Steckdose mit IP: {ip}")
|
||||
|
||||
try:
|
||||
# Wichtig: Verwende PyP100 (nicht PyP110) wie in den funktionierenden Versionen
|
||||
p100 = PyP100.P100(ip, username, password)
|
||||
|
||||
# Handshake und Login
|
||||
log(" 🔄 Handshake...")
|
||||
p100.handshake()
|
||||
|
||||
log(" 🔄 Login...")
|
||||
p100.login()
|
||||
|
||||
# Status abfragen
|
||||
log(" 🔄 Status abfragen...")
|
||||
device_info = p100.getDeviceInfo()
|
||||
|
||||
# Erfolg!
|
||||
state = "Eingeschaltet" if device_info.get("device_on", False) else "Ausgeschaltet"
|
||||
log(f"✅ ERFOLG! Steckdose {ip} erfolgreich verbunden")
|
||||
log(f" 📱 Name: {device_info.get('nickname', 'Unbekannt')}")
|
||||
log(f" ⚡ Status: {state}")
|
||||
|
||||
# Steckdose ein-/ausschalten wenn gewünscht
|
||||
if "--toggle" in sys.argv:
|
||||
if device_info.get("device_on", False):
|
||||
log(" 🔄 Schalte Steckdose AUS...")
|
||||
p100.turnOff()
|
||||
else:
|
||||
log(" 🔄 Schalte Steckdose EIN...")
|
||||
p100.turnOn()
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
# Neuen Status abrufen
|
||||
device_info = p100.getDeviceInfo()
|
||||
state = "Eingeschaltet" if device_info.get("device_on", False) else "Ausgeschaltet"
|
||||
log(f" ⚡ Neuer Status: {state}")
|
||||
|
||||
success = True
|
||||
|
||||
# Konfiguration für settings.py ausgeben
|
||||
log("\n✅ KONFIGURATION FÜR SETTINGS.PY:")
|
||||
log(f"""
|
||||
# TP-Link Tapo Standard-Anmeldedaten
|
||||
TAPO_USERNAME = "{username}"
|
||||
TAPO_PASSWORD = "{password}"
|
||||
|
||||
# Standard-Steckdosen-IPs
|
||||
DEFAULT_TAPO_IPS = ["{ip}"]
|
||||
""")
|
||||
|
||||
# Nur die erste erfolgreiche Steckdose testen
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
log(f"❌ Fehler bei Steckdose {ip}: {str(e)}")
|
||||
|
||||
if not success:
|
||||
log("\n❌ Keine Tapo-Steckdose konnte verbunden werden!")
|
||||
log("Prüfen Sie folgende mögliche Ursachen:")
|
||||
log("1. Steckdosen sind nicht eingesteckt oder mit dem WLAN verbunden")
|
||||
log("2. IP-Adressen sind falsch")
|
||||
log("3. Anmeldedaten sind falsch (prüfen Sie das 'A' am Ende des Passworts)")
|
||||
log("4. Netzwerkprobleme verhindern den Zugriff")
|
||||
|
||||
return success
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("\n====== TAPO P110 SOFORT-TEST ======\n")
|
||||
test_connection()
|
||||
print("\n====== TEST BEENDET ======\n")
|
@ -1,295 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MYP Platform - Requirements Update Script
|
||||
Aktualisiert die Requirements basierend auf tatsächlich verwendeten Imports
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import ast
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
from typing import Set, List, Dict
|
||||
|
||||
def get_imports_from_file(file_path: Path) -> Set[str]:
|
||||
"""Extrahiert alle Import-Statements aus einer Python-Datei."""
|
||||
imports = set()
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
tree = ast.parse(content)
|
||||
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
imports.add(alias.name.split('.')[0])
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
if node.module:
|
||||
imports.add(node.module.split('.')[0])
|
||||
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Parsen von {file_path}: {e}")
|
||||
|
||||
return imports
|
||||
|
||||
def get_all_imports(project_root: Path) -> Set[str]:
|
||||
"""Sammelt alle Imports aus dem Projekt."""
|
||||
all_imports = set()
|
||||
|
||||
# Wichtige Dateien analysieren
|
||||
important_files = [
|
||||
'app.py',
|
||||
'models.py',
|
||||
'utils/rate_limiter.py',
|
||||
'utils/job_scheduler.py',
|
||||
'utils/queue_manager.py',
|
||||
'utils/ssl_manager.py',
|
||||
'utils/security.py',
|
||||
'utils/permissions.py',
|
||||
'utils/analytics.py',
|
||||
'utils/template_helpers.py',
|
||||
'utils/logging_config.py'
|
||||
]
|
||||
|
||||
for file_path in important_files:
|
||||
full_path = project_root / file_path
|
||||
if full_path.exists():
|
||||
imports = get_imports_from_file(full_path)
|
||||
all_imports.update(imports)
|
||||
print(f"✓ Analysiert: {file_path} ({len(imports)} Imports)")
|
||||
|
||||
return all_imports
|
||||
|
||||
def get_package_mapping() -> Dict[str, str]:
|
||||
"""Mapping von Import-Namen zu PyPI-Paketnamen."""
|
||||
return {
|
||||
'flask': 'Flask',
|
||||
'flask_login': 'Flask-Login',
|
||||
'flask_wtf': 'Flask-WTF',
|
||||
'sqlalchemy': 'SQLAlchemy',
|
||||
'werkzeug': 'Werkzeug',
|
||||
'bcrypt': 'bcrypt',
|
||||
'cryptography': 'cryptography',
|
||||
'PyP100': 'PyP100',
|
||||
'redis': 'redis',
|
||||
'requests': 'requests',
|
||||
'jinja2': 'Jinja2',
|
||||
'markupsafe': 'MarkupSafe',
|
||||
'itsdangerous': 'itsdangerous',
|
||||
'psutil': 'psutil',
|
||||
'click': 'click',
|
||||
'blinker': 'blinker',
|
||||
'pywin32': 'pywin32',
|
||||
'pytest': 'pytest',
|
||||
'gunicorn': 'gunicorn'
|
||||
}
|
||||
|
||||
def get_current_versions() -> Dict[str, str]:
|
||||
"""Holt die aktuell installierten Versionen."""
|
||||
versions = {}
|
||||
|
||||
try:
|
||||
result = subprocess.run(['pip', 'list', '--format=freeze'],
|
||||
capture_output=True, text=True,
|
||||
encoding='utf-8', errors='replace')
|
||||
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
if '==' in line:
|
||||
package, version = line.split('==', 1)
|
||||
versions[package.lower()] = version
|
||||
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Abrufen der Versionen: {e}")
|
||||
|
||||
return versions
|
||||
|
||||
def check_package_availability(package: str, version: str = None) -> bool:
|
||||
"""Prüft, ob ein Paket in der angegebenen Version verfügbar ist."""
|
||||
try:
|
||||
if version:
|
||||
cmd = ['pip', 'index', 'versions', package]
|
||||
else:
|
||||
cmd = ['pip', 'show', package]
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True,
|
||||
encoding='utf-8', errors='replace')
|
||||
return result.returncode == 0
|
||||
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def generate_requirements(imports: Set[str], versions: Dict[str, str]) -> List[str]:
|
||||
"""Generiert die Requirements-Liste."""
|
||||
package_mapping = get_package_mapping()
|
||||
requirements = []
|
||||
|
||||
# Standard-Bibliotheken, die nicht installiert werden müssen
|
||||
stdlib_modules = {
|
||||
'os', 'sys', 'logging', 'atexit', 'datetime', 'time', 'subprocess',
|
||||
'json', 'signal', 'threading', 'functools', 'typing', 'contextlib',
|
||||
'secrets', 'hashlib', 'calendar', 'random', 'socket', 'ipaddress',
|
||||
'enum', 'dataclasses', 'concurrent', 'collections'
|
||||
}
|
||||
|
||||
# Lokale Module ausschließen
|
||||
local_modules = {
|
||||
'models', 'utils', 'config', 'blueprints'
|
||||
}
|
||||
|
||||
# Nur externe Pakete berücksichtigen
|
||||
external_imports = imports - stdlib_modules - local_modules
|
||||
|
||||
for import_name in sorted(external_imports):
|
||||
package_name = package_mapping.get(import_name, import_name)
|
||||
|
||||
# Version aus installierten Paketen holen
|
||||
version = versions.get(package_name.lower())
|
||||
|
||||
if version and check_package_availability(package_name, version):
|
||||
if package_name == 'pywin32':
|
||||
requirements.append(f"{package_name}=={version}; sys_platform == \"win32\"")
|
||||
else:
|
||||
requirements.append(f"{package_name}=={version}")
|
||||
else:
|
||||
print(f"⚠️ Paket {package_name} nicht gefunden oder Version unbekannt")
|
||||
|
||||
return requirements
|
||||
|
||||
def write_requirements_file(requirements: List[str], output_file: Path):
|
||||
"""Schreibt die Requirements in eine Datei."""
|
||||
header = """# MYP Platform - Python Dependencies
|
||||
# Basierend auf tatsächlich verwendeten Imports in app.py
|
||||
# Automatisch generiert am: {date}
|
||||
# Installiere mit: pip install -r requirements.txt
|
||||
|
||||
# ===== CORE FLASK FRAMEWORK =====
|
||||
# Direkt in app.py verwendet
|
||||
{flask_requirements}
|
||||
|
||||
# ===== DATENBANK =====
|
||||
# SQLAlchemy für Datenbankoperationen (models.py, app.py)
|
||||
{db_requirements}
|
||||
|
||||
# ===== SICHERHEIT UND AUTHENTIFIZIERUNG =====
|
||||
# Werkzeug für Passwort-Hashing und Utilities (app.py)
|
||||
{security_requirements}
|
||||
|
||||
# ===== SMART PLUG STEUERUNG =====
|
||||
# PyP100 für TP-Link Tapo Smart Plugs (utils/job_scheduler.py)
|
||||
{smartplug_requirements}
|
||||
|
||||
# ===== RATE LIMITING UND CACHING =====
|
||||
# Redis für Rate Limiting (utils/rate_limiter.py) - optional
|
||||
{cache_requirements}
|
||||
|
||||
# ===== HTTP REQUESTS =====
|
||||
# Requests für HTTP-Anfragen (utils/queue_manager.py, utils/debug_drucker_erkennung.py)
|
||||
{http_requirements}
|
||||
|
||||
# ===== TEMPLATE ENGINE =====
|
||||
# Jinja2 und MarkupSafe (automatisch mit Flask installiert, aber explizit für utils/template_helpers.py)
|
||||
{template_requirements}
|
||||
|
||||
# ===== SYSTEM MONITORING =====
|
||||
# psutil für System-Monitoring (utils/debug_utils.py, utils/debug_cli.py)
|
||||
{monitoring_requirements}
|
||||
|
||||
# ===== ZUSÄTZLICHE CORE ABHÄNGIGKEITEN =====
|
||||
# Click für CLI-Kommandos (automatisch mit Flask)
|
||||
{core_requirements}
|
||||
|
||||
# ===== WINDOWS-SPEZIFISCHE ABHÄNGIGKEITEN =====
|
||||
# Nur für Windows-Systeme erforderlich
|
||||
{windows_requirements}
|
||||
|
||||
# ===== OPTIONAL: ENTWICKLUNG UND TESTING =====
|
||||
# Nur für Entwicklungsumgebung
|
||||
{dev_requirements}
|
||||
|
||||
# ===== OPTIONAL: PRODUKTIONS-SERVER =====
|
||||
# Gunicorn für Produktionsumgebung
|
||||
{prod_requirements}
|
||||
"""
|
||||
|
||||
# Requirements kategorisieren
|
||||
flask_reqs = [r for r in requirements if any(x in r.lower() for x in ['flask'])]
|
||||
db_reqs = [r for r in requirements if 'SQLAlchemy' in r]
|
||||
security_reqs = [r for r in requirements if any(x in r for x in ['Werkzeug', 'bcrypt', 'cryptography'])]
|
||||
smartplug_reqs = [r for r in requirements if 'PyP100' in r]
|
||||
cache_reqs = [r for r in requirements if 'redis' in r]
|
||||
http_reqs = [r for r in requirements if 'requests' in r]
|
||||
template_reqs = [r for r in requirements if any(x in r for x in ['Jinja2', 'MarkupSafe', 'itsdangerous'])]
|
||||
monitoring_reqs = [r for r in requirements if 'psutil' in r]
|
||||
core_reqs = [r for r in requirements if any(x in r for x in ['click', 'blinker'])]
|
||||
windows_reqs = [r for r in requirements if 'sys_platform' in r]
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
content = header.format(
|
||||
date=datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
flask_requirements='\n'.join(flask_reqs) or '# Keine Flask-spezifischen Requirements',
|
||||
db_requirements='\n'.join(db_reqs) or '# Keine Datenbank-Requirements',
|
||||
security_requirements='\n'.join(security_reqs) or '# Keine Sicherheits-Requirements',
|
||||
smartplug_requirements='\n'.join(smartplug_reqs) or '# Keine Smart Plug Requirements',
|
||||
cache_requirements='\n'.join(cache_reqs) or '# Keine Cache-Requirements',
|
||||
http_requirements='\n'.join(http_reqs) or '# Keine HTTP-Requirements',
|
||||
template_requirements='\n'.join(template_reqs) or '# Keine Template-Requirements',
|
||||
monitoring_requirements='\n'.join(monitoring_reqs) or '# Keine Monitoring-Requirements',
|
||||
core_requirements='\n'.join(core_reqs) or '# Keine Core-Requirements',
|
||||
windows_requirements='\n'.join(windows_reqs) or '# Keine Windows-Requirements',
|
||||
dev_requirements='pytest==8.3.4; extra == "dev"\npytest-cov==6.0.0; extra == "dev"',
|
||||
prod_requirements='gunicorn==23.0.0; extra == "prod"'
|
||||
)
|
||||
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
def main():
|
||||
"""Hauptfunktion."""
|
||||
print("🔄 MYP Platform Requirements Update")
|
||||
print("=" * 50)
|
||||
|
||||
# Projekt-Root ermitteln
|
||||
project_root = Path(__file__).parent
|
||||
|
||||
print(f"📁 Projekt-Verzeichnis: {project_root}")
|
||||
|
||||
# Imports sammeln
|
||||
print("\n📋 Sammle Imports aus wichtigen Dateien...")
|
||||
imports = get_all_imports(project_root)
|
||||
|
||||
print(f"\n📦 Gefundene externe Imports: {len(imports)}")
|
||||
for imp in sorted(imports):
|
||||
print(f" - {imp}")
|
||||
|
||||
# Aktuelle Versionen abrufen
|
||||
print("\n🔍 Prüfe installierte Versionen...")
|
||||
versions = get_current_versions()
|
||||
|
||||
# Requirements generieren
|
||||
print("\n⚙️ Generiere Requirements...")
|
||||
requirements = generate_requirements(imports, versions)
|
||||
|
||||
# Requirements-Datei schreiben
|
||||
output_file = project_root / 'requirements.txt'
|
||||
write_requirements_file(requirements, output_file)
|
||||
|
||||
print(f"\n✅ Requirements aktualisiert: {output_file}")
|
||||
print(f"📊 {len(requirements)} Pakete in requirements.txt")
|
||||
|
||||
# Zusammenfassung
|
||||
print("\n📋 Generierte Requirements:")
|
||||
for req in requirements:
|
||||
print(f" - {req}")
|
||||
|
||||
print("\n🎉 Requirements-Update abgeschlossen!")
|
||||
print("\nNächste Schritte:")
|
||||
print("1. pip install -r requirements.txt")
|
||||
print("2. Anwendung testen")
|
||||
print("3. requirements-dev.txt und requirements-prod.txt bei Bedarf anpassen")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Reference in New Issue
Block a user