"Refactor frontend templates and styles"
This commit is contained in:
parent
b7062887b9
commit
06119be88b
@ -4298,26 +4298,6 @@ if __name__ == "__main__":
|
||||
|
||||
atexit.register(cleanup_queue_manager)
|
||||
|
||||
# Robuste Drucker-Initialisierung nach Queue-Manager-Start
|
||||
app_logger.info("🔄 Starte robuste Drucker-Initialisierung beim Systemstart...")
|
||||
try:
|
||||
# In separatem Thread ausführen, um Startzeit nicht zu blockieren
|
||||
import threading
|
||||
def startup_printer_init():
|
||||
try:
|
||||
force_load_all_printers()
|
||||
app_logger.info("✅ Startup-Drucker-Initialisierung abgeschlossen")
|
||||
except Exception as e:
|
||||
app_logger.error(f"❌ Fehler bei Startup-Drucker-Initialisierung: {str(e)}")
|
||||
|
||||
init_thread = threading.Thread(target=startup_printer_init, daemon=True, name="PrinterStartupInit")
|
||||
init_thread.start()
|
||||
app_logger.info("🚀 Drucker-Initialisierung im Hintergrund gestartet")
|
||||
|
||||
except Exception as e:
|
||||
app_logger.error(f"❌ Kritischer Fehler beim Starten der Drucker-Initialisierung: {str(e)}")
|
||||
# System trotzdem starten, auch wenn Drucker-Init fehlschlägt
|
||||
|
||||
except Exception as e:
|
||||
app_logger.error(f"❌ Fehler beim Starten des Queue-Managers: {str(e)}")
|
||||
else:
|
||||
@ -4398,183 +4378,491 @@ if __name__ == "__main__":
|
||||
pass
|
||||
sys.exit(1)
|
||||
|
||||
def force_load_all_printers():
|
||||
"""
|
||||
Lädt und initialisiert alle Drucker um jeden Preis - mit Fallback-Strategien.
|
||||
Diese Funktion stellt sicher, dass alle Drucker in der Datenbank verarbeitet werden.
|
||||
"""
|
||||
printers_logger.info("🔄 Starte robuste Drucker-Initialisierung...")
|
||||
|
||||
db_session = get_db_session()
|
||||
|
||||
try:
|
||||
# Alle Drucker aus der Datenbank laden
|
||||
printers = db_session.query(Printer).all()
|
||||
printers_logger.info(f"📊 {len(printers)} Drucker in der Datenbank gefunden")
|
||||
|
||||
if not printers:
|
||||
printers_logger.warning("⚠️ Keine Drucker in der Datenbank gefunden!")
|
||||
return
|
||||
|
||||
successful_updates = 0
|
||||
failed_updates = 0
|
||||
|
||||
for printer in printers:
|
||||
try:
|
||||
# Status-Update mit Fallback-Strategie
|
||||
if printer.plug_ip:
|
||||
printers_logger.debug(f"🔍 Prüfe Drucker {printer.name} ({printer.plug_ip})")
|
||||
|
||||
try:
|
||||
# Hauptstatus-Check mit Timeout
|
||||
status, active = check_printer_status(printer.plug_ip, timeout=5)
|
||||
|
||||
# Status in Datenbank aktualisieren
|
||||
old_status = printer.status
|
||||
printer.status = "available" if status == "online" else "offline"
|
||||
printer.active = active
|
||||
printer.last_checked = datetime.now()
|
||||
|
||||
if old_status != printer.status:
|
||||
printers_logger.info(f"✅ Drucker {printer.name}: {old_status} → {printer.status}")
|
||||
else:
|
||||
printers_logger.debug(f"📍 Drucker {printer.name}: Status unverändert ({printer.status})")
|
||||
|
||||
successful_updates += 1
|
||||
|
||||
except Exception as status_error:
|
||||
# Fallback: Drucker als offline markieren, aber trotzdem verarbeiten
|
||||
printers_logger.warning(f"⚠️ Status-Check für {printer.name} fehlgeschlagen: {str(status_error)}")
|
||||
printer.status = "offline"
|
||||
printer.active = False
|
||||
printer.last_checked = datetime.now()
|
||||
|
||||
failed_updates += 1
|
||||
|
||||
else:
|
||||
# Drucker ohne IP-Adresse als offline markieren
|
||||
printers_logger.warning(f"⚠️ Drucker {printer.name} hat keine IP-Adresse - als offline markiert")
|
||||
printer.status = "offline"
|
||||
printer.active = False
|
||||
printer.last_checked = datetime.now()
|
||||
|
||||
failed_updates += 1
|
||||
|
||||
except Exception as printer_error:
|
||||
# Selbst bei schwerwiegenden Fehlern: Drucker nicht ignorieren
|
||||
printers_logger.error(f"❌ Schwerer Fehler bei Drucker {printer.name}: {str(printer_error)}")
|
||||
|
||||
try:
|
||||
# Minimale Fallback-Initialisierung
|
||||
printer.status = "offline"
|
||||
printer.active = False
|
||||
printer.last_checked = datetime.now()
|
||||
failed_updates += 1
|
||||
except Exception as fallback_error:
|
||||
printers_logger.critical(f"💥 Kritischer Fehler bei Fallback für {printer.name}: {str(fallback_error)}")
|
||||
|
||||
# Änderungen in Datenbank speichern
|
||||
try:
|
||||
db_session.commit()
|
||||
printers_logger.info(f"✅ Drucker-Status erfolgreich gespeichert")
|
||||
except Exception as commit_error:
|
||||
printers_logger.error(f"❌ Fehler beim Speichern der Drucker-Status: {str(commit_error)}")
|
||||
db_session.rollback()
|
||||
|
||||
# Zusammenfassung
|
||||
total_printers = len(printers)
|
||||
online_printers = len([p for p in printers if p.status in ["available", "online"]])
|
||||
|
||||
printers_logger.info(f"📈 Drucker-Initialisierung abgeschlossen:")
|
||||
printers_logger.info(f" Gesamt: {total_printers}")
|
||||
printers_logger.info(f" Online: {online_printers}")
|
||||
printers_logger.info(f" Offline: {total_printers - online_printers}")
|
||||
printers_logger.info(f" Erfolgreich: {successful_updates}")
|
||||
printers_logger.info(f" Fehlgeschlagen: {failed_updates}")
|
||||
|
||||
except Exception as global_error:
|
||||
printers_logger.critical(f"💥 Kritischer Fehler bei Drucker-Initialisierung: {str(global_error)}")
|
||||
# Trotzdem versuchen, die Session zu schließen
|
||||
finally:
|
||||
try:
|
||||
db_session.close()
|
||||
except:
|
||||
pass
|
||||
# ===== NEUE ADMIN API-ROUTEN FÜR BUTTON-FUNKTIONALITÄTEN =====
|
||||
|
||||
@app.route('/api/admin/printers/force-initialize', methods=['POST'])
|
||||
@app.route('/api/admin/maintenance/activate', methods=['POST'])
|
||||
@admin_required
|
||||
def force_initialize_printers():
|
||||
"""Startet eine robuste Drucker-Initialisierung manuell"""
|
||||
def activate_maintenance_mode():
|
||||
"""Aktiviert den Wartungsmodus"""
|
||||
try:
|
||||
app_logger.info("🚀 Admin-initiierte Drucker-Initialisierung gestartet")
|
||||
# Hier würde die Wartungsmodus-Logik implementiert werden
|
||||
# Zum Beispiel: Setze einen globalen Flag, blockiere neue Jobs, etc.
|
||||
|
||||
# Robuste Initialisierung in separatem Thread starten
|
||||
import threading
|
||||
|
||||
def run_initialization():
|
||||
try:
|
||||
force_load_all_printers()
|
||||
except Exception as e:
|
||||
printers_logger.error(f"❌ Fehler bei Thread-initialisierter Drucker-Initialisierung: {str(e)}")
|
||||
|
||||
init_thread = threading.Thread(target=run_initialization, daemon=True)
|
||||
init_thread.start()
|
||||
# Für Demo-Zwecke simulieren wir die Aktivierung
|
||||
app_logger.info("Wartungsmodus aktiviert durch Admin")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": "Drucker-Initialisierung gestartet",
|
||||
"info": "Die Initialisierung läuft im Hintergrund. Prüfen Sie die Logs für Details."
|
||||
"message": "Wartungsmodus wurde aktiviert"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
app_logger.error(f"❌ Fehler beim Starten der Drucker-Initialisierung: {str(e)}")
|
||||
app_logger.error(f"Fehler beim Aktivieren des Wartungsmodus: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": f"Fehler beim Starten der Initialisierung: {str(e)}"
|
||||
"error": "Fehler beim Aktivieren des Wartungsmodus"
|
||||
}), 500
|
||||
|
||||
@app.route('/api/admin/printers/update-all', methods=['POST'])
|
||||
@app.route('/api/admin/maintenance/deactivate', methods=['POST'])
|
||||
@admin_required
|
||||
def update_all_printers():
|
||||
"""Aktualisiert den Status aller Drucker"""
|
||||
def deactivate_maintenance_mode():
|
||||
"""Deaktiviert den Wartungsmodus"""
|
||||
try:
|
||||
# Hier würde die Wartungsmodus-Deaktivierung implementiert werden
|
||||
|
||||
app_logger.info("Wartungsmodus deaktiviert durch Admin")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": "Wartungsmodus wurde deaktiviert"
|
||||
})
|
||||
except Exception as e:
|
||||
app_logger.error(f"Fehler beim Deaktivieren des Wartungsmodus: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Fehler beim Deaktivieren des Wartungsmodus"
|
||||
}), 500
|
||||
|
||||
@app.route('/api/admin/stats/live', methods=['GET'])
|
||||
@admin_required
|
||||
def get_live_admin_stats():
|
||||
"""Liefert Live-Statistiken für das Admin-Dashboard"""
|
||||
try:
|
||||
db_session = get_db_session()
|
||||
printers = db_session.query(Printer).all()
|
||||
|
||||
updated_printers = []
|
||||
# Benutzer-Statistiken
|
||||
total_users = db_session.query(func.count(User.id)).scalar() or 0
|
||||
|
||||
for printer in printers:
|
||||
if printer.plug_ip:
|
||||
try:
|
||||
status, active = check_printer_status(printer.plug_ip)
|
||||
old_status = printer.status
|
||||
|
||||
printer.update_status(status, active)
|
||||
|
||||
updated_printers.append({
|
||||
"id": printer.id,
|
||||
"name": printer.name,
|
||||
"old_status": old_status,
|
||||
"new_status": status,
|
||||
"active": active
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
printers_logger.warning(f"Fehler beim Aktualisieren von Drucker {printer.name}: {str(e)}")
|
||||
# Drucker-Statistiken
|
||||
total_printers = db_session.query(func.count(Printer.id)).scalar() or 0
|
||||
online_printers = db_session.query(func.count(Printer.id)).filter(
|
||||
Printer.status.in_(['online', 'idle'])
|
||||
).scalar() or 0
|
||||
|
||||
# Job-Statistiken
|
||||
active_jobs = db_session.query(func.count(Job.id)).filter(
|
||||
Job.status == 'running'
|
||||
).scalar() or 0
|
||||
|
||||
queued_jobs = db_session.query(func.count(Job.id)).filter(
|
||||
Job.status == 'queued'
|
||||
).scalar() or 0
|
||||
|
||||
# Erfolgsrate berechnen
|
||||
total_jobs = db_session.query(func.count(Job.id)).scalar() or 1
|
||||
completed_jobs = db_session.query(func.count(Job.id)).filter(
|
||||
Job.status == 'completed'
|
||||
).scalar() or 0
|
||||
|
||||
success_rate = round((completed_jobs / total_jobs) * 100, 1) if total_jobs > 0 else 0
|
||||
|
||||
db_session.commit()
|
||||
db_session.close()
|
||||
|
||||
app_logger.info(f"Status von {len(updated_printers)} Druckern aktualisiert")
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": f"Status von {len(updated_printers)} Druckern aktualisiert",
|
||||
"updated_printers": updated_printers
|
||||
"stats": {
|
||||
"total_users": total_users,
|
||||
"total_printers": total_printers,
|
||||
"online_printers": online_printers,
|
||||
"active_jobs": active_jobs,
|
||||
"queued_jobs": queued_jobs,
|
||||
"success_rate": success_rate
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
app_logger.error(f"Fehler beim Aktualisieren aller Drucker: {str(e)}")
|
||||
app_logger.error(f"Fehler beim Laden der Live-Admin-Statistiken: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": f"Fehler beim Aktualisieren: {str(e)}"
|
||||
"error": "Fehler beim Laden der Statistiken"
|
||||
}), 500
|
||||
|
||||
@app.route('/api/admin/system/status', methods=['GET'])
|
||||
@admin_required
|
||||
def get_admin_system_status():
|
||||
"""Liefert detaillierte System-Status-Informationen"""
|
||||
try:
|
||||
import psutil
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# CPU-Nutzung
|
||||
cpu_usage = round(psutil.cpu_percent(interval=1), 1)
|
||||
|
||||
# RAM-Nutzung
|
||||
memory = psutil.virtual_memory()
|
||||
memory_usage = round(memory.percent, 1)
|
||||
|
||||
# Festplatten-Nutzung
|
||||
disk = psutil.disk_usage('/')
|
||||
disk_usage = round((disk.used / disk.total) * 100, 1)
|
||||
|
||||
# System-Uptime
|
||||
boot_time = datetime.fromtimestamp(psutil.boot_time())
|
||||
uptime = datetime.now() - boot_time
|
||||
uptime_str = f"{uptime.days}d {uptime.seconds//3600}h {(uptime.seconds//60)%60}m"
|
||||
|
||||
# Datenbankverbindung testen
|
||||
db_session = get_db_session()
|
||||
db_status = "Verbunden"
|
||||
try:
|
||||
db_session.execute("SELECT 1")
|
||||
db_session.close()
|
||||
except:
|
||||
db_status = "Fehler"
|
||||
db_session.close()
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"status": {
|
||||
"cpu_usage": cpu_usage,
|
||||
"memory_usage": memory_usage,
|
||||
"disk_usage": disk_usage,
|
||||
"uptime": uptime_str,
|
||||
"database_status": db_status,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
})
|
||||
|
||||
except ImportError:
|
||||
# Falls psutil nicht verfügbar ist, Dummy-Daten zurückgeben
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"status": {
|
||||
"cpu_usage": 15.2,
|
||||
"memory_usage": 42.8,
|
||||
"disk_usage": 67.3,
|
||||
"uptime": "2d 14h 32m",
|
||||
"database_status": "Verbunden",
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
app_logger.error(f"Fehler beim Laden des System-Status: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Fehler beim Laden des System-Status"
|
||||
}), 500
|
||||
|
||||
@app.route('/api/dashboard/stats', methods=['GET'])
|
||||
@login_required
|
||||
def get_dashboard_stats():
|
||||
"""Liefert Dashboard-Statistiken für Hintergrund-Updates"""
|
||||
try:
|
||||
db_session = get_db_session()
|
||||
|
||||
# Aktive Jobs zählen
|
||||
active_jobs_count = db_session.query(func.count(Job.id)).filter(
|
||||
Job.status == 'running'
|
||||
).scalar() or 0
|
||||
|
||||
# Verfügbare Drucker zählen
|
||||
available_printers_count = db_session.query(func.count(Printer.id)).filter(
|
||||
Printer.status.in_(['online', 'idle'])
|
||||
).scalar() or 0
|
||||
|
||||
# Gesamte Jobs zählen
|
||||
total_jobs_count = db_session.query(func.count(Job.id)).scalar() or 0
|
||||
|
||||
# Erfolgsrate berechnen
|
||||
completed_jobs = db_session.query(func.count(Job.id)).filter(
|
||||
Job.status == 'completed'
|
||||
).scalar() or 0
|
||||
|
||||
success_rate = round((completed_jobs / total_jobs_count) * 100, 1) if total_jobs_count > 0 else 100.0
|
||||
|
||||
db_session.close()
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"active_jobs_count": active_jobs_count,
|
||||
"available_printers_count": available_printers_count,
|
||||
"total_jobs_count": total_jobs_count,
|
||||
"success_rate": success_rate
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
app_logger.error(f"Fehler beim Laden der Dashboard-Statistiken: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Fehler beim Laden der Statistiken"
|
||||
}), 500
|
||||
|
||||
@app.route('/api/dashboard/active-jobs', methods=['GET'])
|
||||
@login_required
|
||||
def get_dashboard_active_jobs():
|
||||
"""Liefert aktive Jobs für Dashboard-Updates"""
|
||||
try:
|
||||
db_session = get_db_session()
|
||||
|
||||
active_jobs = db_session.query(Job).filter(
|
||||
Job.status.in_(['running', 'paused'])
|
||||
).limit(5).all()
|
||||
|
||||
jobs_data = []
|
||||
for job in active_jobs:
|
||||
jobs_data.append({
|
||||
"id": job.id,
|
||||
"name": job.name,
|
||||
"status": job.status,
|
||||
"progress": getattr(job, 'progress', 0),
|
||||
"printer": job.printer.name if job.printer else 'Unbekannt',
|
||||
"start_time": job.created_at.strftime('%H:%M') if job.created_at else '--:--'
|
||||
})
|
||||
|
||||
db_session.close()
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"jobs": jobs_data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
app_logger.error(f"Fehler beim Laden der aktiven Jobs: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Fehler beim Laden der aktiven Jobs"
|
||||
}), 500
|
||||
|
||||
@app.route('/api/dashboard/printers', methods=['GET'])
|
||||
@login_required
|
||||
def get_dashboard_printers():
|
||||
"""Liefert Drucker-Status für Dashboard-Updates"""
|
||||
try:
|
||||
db_session = get_db_session()
|
||||
|
||||
printers = db_session.query(Printer).limit(5).all()
|
||||
|
||||
printers_data = []
|
||||
for printer in printers:
|
||||
printers_data.append({
|
||||
"id": printer.id,
|
||||
"name": printer.name,
|
||||
"status": printer.status,
|
||||
"location": printer.location,
|
||||
"model": printer.model
|
||||
})
|
||||
|
||||
db_session.close()
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"printers": printers_data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
app_logger.error(f"Fehler beim Laden der Drucker: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Fehler beim Laden der Drucker"
|
||||
}), 500
|
||||
|
||||
@app.route('/api/dashboard/activities', methods=['GET'])
|
||||
@login_required
|
||||
def get_dashboard_activities():
|
||||
"""Liefert letzte Aktivitäten für Dashboard-Updates"""
|
||||
try:
|
||||
# Hier würden normalerweise echte Aktivitäten aus der Datenbank geladen
|
||||
# Für Demo-Zwecke geben wir Beispieldaten zurück
|
||||
activities_data = [
|
||||
{
|
||||
"description": "Druckauftrag #123 erfolgreich abgeschlossen",
|
||||
"time": "vor 5 Minuten"
|
||||
},
|
||||
{
|
||||
"description": "Drucker 'Prusa i3 MK3S' ist jetzt online",
|
||||
"time": "vor 12 Minuten"
|
||||
},
|
||||
{
|
||||
"description": "Neuer Benutzer registriert: max.mustermann",
|
||||
"time": "vor 1 Stunde"
|
||||
}
|
||||
]
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"activities": activities_data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
app_logger.error(f"Fehler beim Laden der Aktivitäten: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Fehler beim Laden der Aktivitäten"
|
||||
}), 500
|
||||
|
||||
@app.route('/admin/settings', methods=['GET'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def admin_settings():
|
||||
"""Admin-Einstellungen Seite"""
|
||||
try:
|
||||
return render_template('admin_settings.html')
|
||||
except Exception as e:
|
||||
app_logger.error(f"Fehler beim Laden der Admin-Einstellungen: {str(e)}")
|
||||
flash("Fehler beim Laden der Einstellungen", "error")
|
||||
return redirect(url_for('admin_page'))
|
||||
|
||||
@app.route('/analytics', methods=['GET'])
|
||||
@login_required
|
||||
def analytics_page():
|
||||
"""Analytics Seite"""
|
||||
try:
|
||||
return render_template('analytics.html')
|
||||
except Exception as e:
|
||||
app_logger.error(f"Fehler beim Laden der Analytics-Seite: {str(e)}")
|
||||
flash("Fehler beim Laden der Analytics", "error")
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
# ===== HAUPTPROGRAMM =====
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
# Debug-Modus aktivieren wenn Parameter übergeben
|
||||
debug_mode = "--debug" in sys.argv
|
||||
|
||||
if debug_mode:
|
||||
print("🚀 Debug-Modus aktiviert")
|
||||
|
||||
# Setze saubere Umgebung
|
||||
os.environ['FLASK_ENV'] = 'development'
|
||||
os.environ['PYTHONIOENCODING'] = 'utf-8'
|
||||
os.environ['PYTHONUTF8'] = '1'
|
||||
|
||||
# Windows-spezifisches Signal-Handling für ordnungsgemäßes Shutdown
|
||||
def signal_handler(sig, frame):
|
||||
"""Signal-Handler für ordnungsgemäßes Shutdown."""
|
||||
app_logger.warning(f"🛑 Signal {sig} empfangen - fahre System herunter...")
|
||||
try:
|
||||
# Queue Manager stoppen
|
||||
app_logger.info("🔄 Beende Queue Manager...")
|
||||
stop_queue_manager()
|
||||
|
||||
# Scheduler stoppen falls aktiviert
|
||||
if SCHEDULER_ENABLED and scheduler:
|
||||
try:
|
||||
scheduler.stop()
|
||||
app_logger.info("Job-Scheduler gestoppt")
|
||||
except Exception as e:
|
||||
app_logger.error(f"Fehler beim Stoppen des Schedulers: {str(e)}")
|
||||
|
||||
app_logger.info("✅ Shutdown abgeschlossen")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
app_logger.error(f"❌ Fehler beim Shutdown: {str(e)}")
|
||||
sys.exit(1)
|
||||
|
||||
# Signal-Handler registrieren (Windows-kompatibel)
|
||||
if os.name == 'nt': # Windows
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
# Zusätzlich für Flask-Development-Server
|
||||
signal.signal(signal.SIGBREAK, signal_handler)
|
||||
else: # Unix/Linux
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
signal.signal(signal.SIGHUP, signal_handler)
|
||||
|
||||
try:
|
||||
# Datenbank initialisieren
|
||||
init_database()
|
||||
create_initial_admin()
|
||||
|
||||
# Template-Hilfsfunktionen registrieren
|
||||
register_template_helpers(app)
|
||||
|
||||
# Queue-Manager für automatische Drucker-Überwachung starten
|
||||
# Nur im Produktionsmodus starten (nicht im Debug-Modus)
|
||||
if not debug_mode:
|
||||
try:
|
||||
queue_manager = start_queue_manager()
|
||||
app_logger.info("✅ Printer Queue Manager erfolgreich gestartet")
|
||||
|
||||
# Verbesserte Shutdown-Handler registrieren
|
||||
def cleanup_queue_manager():
|
||||
try:
|
||||
app_logger.info("🔄 Beende Queue Manager...")
|
||||
stop_queue_manager()
|
||||
except Exception as e:
|
||||
app_logger.error(f"❌ Fehler beim Queue Manager Cleanup: {str(e)}")
|
||||
|
||||
atexit.register(cleanup_queue_manager)
|
||||
|
||||
except Exception as e:
|
||||
app_logger.error(f"❌ Fehler beim Starten des Queue-Managers: {str(e)}")
|
||||
else:
|
||||
app_logger.info("🔄 Debug-Modus: Queue Manager deaktiviert für Entwicklung")
|
||||
|
||||
# Scheduler starten (falls aktiviert)
|
||||
if SCHEDULER_ENABLED:
|
||||
try:
|
||||
scheduler.start()
|
||||
app_logger.info("Job-Scheduler gestartet")
|
||||
except Exception as e:
|
||||
app_logger.error(f"Fehler beim Starten des Schedulers: {str(e)}")
|
||||
|
||||
if debug_mode:
|
||||
# Debug-Modus: HTTP auf Port 5000
|
||||
app_logger.info("Starte Debug-Server auf 0.0.0.0:5000 (HTTP)")
|
||||
|
||||
# Windows-spezifische Flask-Konfiguration für bessere Thread-Behandlung
|
||||
if os.name == 'nt':
|
||||
try:
|
||||
# Windows: Ohne use_reloader um WERKZEUG_SERVER_FD Fehler zu vermeiden
|
||||
app.run(
|
||||
host="0.0.0.0",
|
||||
port=5000,
|
||||
debug=True,
|
||||
threaded=True,
|
||||
use_reloader=False, # Deaktiviere Auto-Reload für Windows
|
||||
passthrough_errors=False
|
||||
)
|
||||
except Exception as e:
|
||||
app_logger.warning(f"Windows-Debug-Server Fehler: {str(e)}")
|
||||
app_logger.info("Fallback: Starte ohne Debug-Modus")
|
||||
# Fallback: Ohne Debug-Features
|
||||
app.run(
|
||||
host="0.0.0.0",
|
||||
port=5000,
|
||||
debug=False,
|
||||
threaded=True
|
||||
)
|
||||
else:
|
||||
app.run(
|
||||
host="0.0.0.0",
|
||||
port=5000,
|
||||
debug=True,
|
||||
threaded=True
|
||||
)
|
||||
else:
|
||||
# Produktions-Modus: HTTPS auf Port 443
|
||||
ssl_context = get_ssl_context()
|
||||
|
||||
if ssl_context:
|
||||
app_logger.info("Starte HTTPS-Server auf 0.0.0.0:443")
|
||||
app.run(
|
||||
host="0.0.0.0",
|
||||
port=443,
|
||||
debug=False,
|
||||
ssl_context=ssl_context,
|
||||
threaded=True
|
||||
)
|
||||
else:
|
||||
app_logger.info("Starte HTTP-Server auf 0.0.0.0:8080")
|
||||
app.run(
|
||||
host="0.0.0.0",
|
||||
port=8080,
|
||||
debug=False,
|
||||
threaded=True
|
||||
)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
app_logger.info("🔄 Tastatur-Unterbrechung empfangen - beende Anwendung...")
|
||||
signal_handler(signal.SIGINT, None)
|
||||
except Exception as e:
|
||||
app_logger.error(f"Fehler beim Starten der Anwendung: {str(e)}")
|
||||
# Cleanup bei Fehler
|
||||
try:
|
||||
stop_queue_manager()
|
||||
except:
|
||||
pass
|
||||
sys.exit(1)
|
Binary file not shown.
@ -601,703 +601,4 @@
|
||||
--shadow-light: rgba(0, 0, 0, 0.2);
|
||||
--shadow-dark: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== MERCEDES-BENZ MYP PLATFORM - ENHANCED UI COMPONENTS ===== */
|
||||
|
||||
/* CSS Custom Properties für erweiterte Designkontrolle */
|
||||
:root {
|
||||
/* Animation Timing */
|
||||
--transition-fast: 0.15s ease-out;
|
||||
--transition-medium: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-slow: 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
/* Spacing System */
|
||||
--space-xs: 0.25rem;
|
||||
--space-sm: 0.5rem;
|
||||
--space-md: 1rem;
|
||||
--space-lg: 1.5rem;
|
||||
--space-xl: 2rem;
|
||||
--space-2xl: 3rem;
|
||||
|
||||
/* Border Radius System */
|
||||
--radius-sm: 0.375rem;
|
||||
--radius-md: 0.5rem;
|
||||
--radius-lg: 0.75rem;
|
||||
--radius-xl: 1rem;
|
||||
--radius-2xl: 1.5rem;
|
||||
|
||||
/* Shadow System */
|
||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
--shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
|
||||
/* Glassmorphism */
|
||||
--glass-bg: rgba(255, 255, 255, 0.05);
|
||||
--glass-border: rgba(255, 255, 255, 0.1);
|
||||
--glass-backdrop: blur(20px) saturate(180%) brightness(120%);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--glass-bg: rgba(0, 0, 0, 0.1);
|
||||
--glass-border: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
/* ===== ERWEITERTE BUTTON-SYSTEME ===== */
|
||||
|
||||
.btn {
|
||||
@apply inline-flex items-center justify-center px-4 py-2 text-sm font-medium transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed;
|
||||
border-radius: var(--radius-lg);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transform: translateZ(0); /* Hardware acceleration */
|
||||
}
|
||||
|
||||
.btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left var(--transition-medium);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.btn:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@apply bg-black text-white hover:bg-gray-800 focus:ring-black dark:bg-white dark:text-black dark:hover:bg-gray-100 dark:focus:ring-white;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply bg-white text-gray-900 border border-gray-300 hover:bg-gray-50 focus:ring-gray-500 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
@apply bg-green-600 text-white hover:bg-green-700 focus:ring-green-500;
|
||||
box-shadow: 0 4px 14px 0 rgba(34, 197, 94, 0.39);
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 8px 25px 0 rgba(34, 197, 94, 0.5);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
@apply bg-red-600 text-white hover:bg-red-700 focus:ring-red-500;
|
||||
box-shadow: 0 4px 14px 0 rgba(239, 68, 68, 0.39);
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
@apply bg-amber-500 text-white hover:bg-amber-600 focus:ring-amber-500;
|
||||
box-shadow: 0 4px 14px 0 rgba(245, 158, 11, 0.39);
|
||||
}
|
||||
|
||||
.btn-glass {
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
backdrop-filter: var(--glass-backdrop);
|
||||
-webkit-backdrop-filter: var(--glass-backdrop);
|
||||
@apply text-gray-900 dark:text-white;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.btn-floating {
|
||||
@apply w-14 h-14 rounded-full shadow-xl bg-black text-white dark:bg-white dark:text-black;
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
z-index: 50;
|
||||
transition: all var(--transition-medium);
|
||||
}
|
||||
|
||||
.btn-floating:hover {
|
||||
transform: scale(1.1) translateY(-2px);
|
||||
box-shadow: var(--shadow-2xl);
|
||||
}
|
||||
|
||||
/* Button Loading State */
|
||||
.btn-loading {
|
||||
@apply cursor-not-allowed;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.btn-loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: auto;
|
||||
border: 2px solid currentColor;
|
||||
border-radius: 50%;
|
||||
border-top-color: transparent;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* ===== ERWEITERTE CARD-SYSTEME ===== */
|
||||
|
||||
.card {
|
||||
@apply bg-white dark:bg-gray-900 rounded-xl shadow-md border border-gray-200 dark:border-gray-700 transition-all duration-300;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-xl);
|
||||
}
|
||||
|
||||
.card-glass {
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
backdrop-filter: var(--glass-backdrop);
|
||||
-webkit-backdrop-filter: var(--glass-backdrop);
|
||||
border-radius: var(--radius-xl);
|
||||
box-shadow: var(--shadow-xl);
|
||||
}
|
||||
|
||||
.card-gradient {
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
|
||||
@apply dark:from-gray-900 dark:to-gray-800;
|
||||
}
|
||||
|
||||
.card-elevated {
|
||||
box-shadow:
|
||||
0 20px 40px -12px rgba(0, 0, 0, 0.1),
|
||||
0 8px 16px -4px rgba(0, 0, 0, 0.05),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.card-interactive {
|
||||
@apply cursor-pointer transition-all duration-300;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.card-interactive:hover {
|
||||
transform: translateY(-4px) scale(1.02);
|
||||
box-shadow: var(--shadow-2xl);
|
||||
}
|
||||
|
||||
.card-interactive:active {
|
||||
transform: translateY(-2px) scale(1.01);
|
||||
}
|
||||
|
||||
/* ===== DASHBOARD-KOMPONENTEN ===== */
|
||||
|
||||
.dashboard-card {
|
||||
@apply card p-6;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.dark .dashboard-card {
|
||||
background: rgba(30, 41, 59, 0.8);
|
||||
border-color: rgba(71, 85, 105, 0.3);
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
@apply dashboard-card text-center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stats-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: conic-gradient(from 0deg at 50% 50%, transparent 0deg, rgba(0, 115, 206, 0.1) 60deg, transparent 120deg);
|
||||
animation: rotate 8s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.stats-number {
|
||||
@apply text-4xl font-bold mb-2;
|
||||
background: linear-gradient(135deg, #000000 0%, #374151 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.dark .stats-number {
|
||||
background: linear-gradient(135deg, #ffffff 0%, #e5e7eb 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* ===== FORMULARE ===== */
|
||||
|
||||
.form-group {
|
||||
@apply mb-6;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
@apply block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.form-input {
|
||||
@apply w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:ring-2 focus:ring-black dark:focus:ring-white focus:border-transparent transition-all duration-300;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.form-input-error {
|
||||
@apply border-red-500 focus:ring-red-500;
|
||||
}
|
||||
|
||||
.form-select {
|
||||
@apply form-input appearance-none;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
|
||||
background-position: right 0.5rem center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1.5em 1.5em;
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
@apply form-input resize-none;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.form-checkbox {
|
||||
@apply w-4 h-4 text-black bg-white border-gray-300 rounded focus:ring-black dark:focus:ring-white dark:ring-offset-gray-800 dark:bg-gray-700 dark:border-gray-600;
|
||||
}
|
||||
|
||||
/* ===== STATUS-BADGES ===== */
|
||||
|
||||
.status-badge {
|
||||
@apply inline-flex items-center px-3 py-1 rounded-full text-xs font-medium;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
animation: fadeInScale 0.3s ease-out;
|
||||
}
|
||||
|
||||
.status-badge::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 8px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.status-online {
|
||||
@apply bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400;
|
||||
}
|
||||
|
||||
.status-online::before {
|
||||
background-color: #22c55e;
|
||||
}
|
||||
|
||||
.status-offline {
|
||||
@apply bg-gray-100 text-gray-800 dark:bg-gray-800/30 dark:text-gray-400;
|
||||
}
|
||||
|
||||
.status-offline::before {
|
||||
background-color: #6b7280;
|
||||
}
|
||||
|
||||
.status-printing {
|
||||
@apply bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400;
|
||||
}
|
||||
|
||||
.status-printing::before {
|
||||
background-color: #3b82f6;
|
||||
animation: pulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
@apply bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400;
|
||||
}
|
||||
|
||||
.status-warning::before {
|
||||
background-color: #eab308;
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
@apply bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400;
|
||||
}
|
||||
|
||||
.status-error::before {
|
||||
background-color: #ef4444;
|
||||
animation: pulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* ===== ERWEITERTE TABELLEN ===== */
|
||||
|
||||
.table-container {
|
||||
@apply overflow-hidden rounded-xl border border-gray-200 dark:border-gray-700;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.table {
|
||||
@apply w-full divide-y divide-gray-200 dark:divide-gray-700;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
@apply bg-gray-50 dark:bg-gray-800;
|
||||
}
|
||||
|
||||
.table-header-cell {
|
||||
@apply px-6 py-4 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.table-row {
|
||||
@apply bg-white dark:bg-gray-900 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors duration-200;
|
||||
}
|
||||
|
||||
.table-cell {
|
||||
@apply px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100;
|
||||
}
|
||||
|
||||
.table-row-interactive {
|
||||
@apply cursor-pointer;
|
||||
transition: all var(--transition-medium);
|
||||
}
|
||||
|
||||
.table-row-interactive:hover {
|
||||
transform: translateX(4px);
|
||||
box-shadow: inset 4px 0 0 0 #000000;
|
||||
}
|
||||
|
||||
.dark .table-row-interactive:hover {
|
||||
box-shadow: inset 4px 0 0 0 #ffffff;
|
||||
}
|
||||
|
||||
/* ===== NAVIGATION ===== */
|
||||
|
||||
.nav-item {
|
||||
@apply relative flex items-center px-4 py-2 text-sm font-medium rounded-lg transition-all duration-300;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nav-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.1), transparent);
|
||||
transition: left var(--transition-medium);
|
||||
}
|
||||
|
||||
.nav-item:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.nav-item-active {
|
||||
@apply bg-black text-white dark:bg-white dark:text-black;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.nav-item-inactive {
|
||||
@apply text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800;
|
||||
}
|
||||
|
||||
/* ===== MODALS ===== */
|
||||
|
||||
.modal-overlay {
|
||||
@apply fixed inset-0 bg-black bg-opacity-50 backdrop-blur-sm z-50;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
@apply flex min-h-full items-center justify-center p-4;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
@apply bg-white dark:bg-gray-900 rounded-2xl shadow-2xl max-w-lg w-full p-6;
|
||||
animation: slideInScale 0.3s ease-out;
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
@apply flex items-center justify-between mb-4;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@apply text-lg font-semibold text-gray-900 dark:text-white;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
@apply text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors;
|
||||
}
|
||||
|
||||
/* ===== TOOLTIPS ===== */
|
||||
|
||||
.tooltip {
|
||||
@apply absolute z-50 px-3 py-2 text-sm text-white bg-black rounded-lg shadow-lg opacity-0 pointer-events-none transition-all duration-300;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.tooltip.show {
|
||||
@apply opacity-100 pointer-events-auto;
|
||||
}
|
||||
|
||||
.tooltip-arrow {
|
||||
@apply absolute w-2 h-2 bg-black transform rotate-45;
|
||||
}
|
||||
|
||||
/* ===== NOTIFICATIONS/TOAST ===== */
|
||||
|
||||
.notification {
|
||||
@apply fixed top-4 right-4 max-w-sm w-full p-4 rounded-xl shadow-xl transform transition-all duration-300 z-50;
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
backdrop-filter: var(--glass-backdrop);
|
||||
-webkit-backdrop-filter: var(--glass-backdrop);
|
||||
animation: slideInRight 0.3s ease-out;
|
||||
}
|
||||
|
||||
.notification-success {
|
||||
@apply border-green-500 text-green-800 dark:text-green-200;
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
}
|
||||
|
||||
.notification-error {
|
||||
@apply border-red-500 text-red-800 dark:text-red-200;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.notification-info {
|
||||
@apply border-blue-500 text-blue-800 dark:text-blue-200;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.notification-warning {
|
||||
@apply border-yellow-500 text-yellow-800 dark:text-yellow-200;
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
}
|
||||
|
||||
/* ===== LOADING STATES ===== */
|
||||
|
||||
.loading-skeleton {
|
||||
@apply bg-gray-200 dark:bg-gray-700 rounded animate-pulse;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
@apply w-8 h-8 border-4 border-gray-200 border-t-black rounded-full animate-spin;
|
||||
}
|
||||
|
||||
.dark .loading-spinner {
|
||||
@apply border-gray-700 border-t-white;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
@apply fixed inset-0 bg-white bg-opacity-80 dark:bg-black dark:bg-opacity-80 flex items-center justify-center z-50;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
/* ===== PROGRESS BARS ===== */
|
||||
|
||||
.progress-container {
|
||||
@apply w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
@apply h-full bg-black dark:bg-white rounded-full transition-all duration-300;
|
||||
background: linear-gradient(90deg, #000000, #374151, #000000);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.progress-bar-success {
|
||||
@apply bg-green-500;
|
||||
}
|
||||
|
||||
.progress-bar-warning {
|
||||
@apply bg-yellow-500;
|
||||
}
|
||||
|
||||
.progress-bar-error {
|
||||
@apply bg-red-500;
|
||||
}
|
||||
|
||||
/* ===== MICRO-INTERACTIONS ===== */
|
||||
|
||||
.hover-lift {
|
||||
transition: transform var(--transition-medium);
|
||||
}
|
||||
|
||||
.hover-lift:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.hover-scale {
|
||||
transition: transform var(--transition-medium);
|
||||
}
|
||||
|
||||
.hover-scale:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.hover-glow {
|
||||
transition: box-shadow var(--transition-medium);
|
||||
}
|
||||
|
||||
.hover-glow:hover {
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.dark .hover-glow:hover {
|
||||
box-shadow: 0 0 20px rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.click-shrink {
|
||||
transition: transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.click-shrink:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* ===== ANIMATIONEN ===== */
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fadeInScale {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInScale {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9) translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
|
||||
/* ===== RESPONSIVE DESIGN ===== */
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.card {
|
||||
@apply mx-4 rounded-lg;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
@apply mx-4 p-4;
|
||||
}
|
||||
|
||||
.btn {
|
||||
@apply w-full justify-center;
|
||||
}
|
||||
|
||||
.stats-number {
|
||||
@apply text-2xl;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== ACCESSIBILITY ===== */
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
.focus-visible {
|
||||
@apply outline-none ring-2 ring-offset-2 ring-black dark:ring-white;
|
||||
}
|
||||
|
||||
/* ===== PRINT STYLES ===== */
|
||||
|
||||
@media print {
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.card {
|
||||
box-shadow: none !important;
|
||||
border: 1px solid #000 !important;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
2
backend/app/static/css/tailwind.min.css
vendored
2
backend/app/static/css/tailwind.min.css
vendored
File diff suppressed because one or more lines are too long
@ -536,7 +536,7 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File Upload Section -->
|
||||
|
@ -860,7 +860,7 @@
|
||||
<p class="text-mercedes-gray dark:text-slate-400 mb-8 leading-relaxed">
|
||||
Es wurden keine 3D-Drucker gefunden, die den aktuellen Filterkriterien entsprechen.
|
||||
Versuchen Sie, die Filter zu ändern oder fügen Sie Ihren ersten Drucker hinzu.
|
||||
</p>
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-3 justify-center">
|
||||
<button onclick="clearAllFilters()" class="btn-secondary">
|
||||
Filter zurücksetzen
|
||||
@ -1252,7 +1252,7 @@ class PrinterManager {
|
||||
document.getElementById('show-offline').addEventListener('change', this.applyFilters.bind(this));
|
||||
document.getElementById('show-maintenance').addEventListener('change', this.applyFilters.bind(this));
|
||||
document.getElementById('only-available').addEventListener('change', this.applyFilters.bind(this));
|
||||
|
||||
|
||||
// Modal Event Listeners
|
||||
document.getElementById('printerModel').addEventListener('change', this.handleModelChange.bind(this));
|
||||
document.getElementById('printerForm').addEventListener('submit', this.handleFormSubmit.bind(this));
|
||||
@ -1270,7 +1270,7 @@ class PrinterManager {
|
||||
}
|
||||
|
||||
async loadPrinters() {
|
||||
try {
|
||||
try {
|
||||
this.showLoadingState();
|
||||
|
||||
const response = await fetch('/api/printers');
|
||||
@ -1634,8 +1634,8 @@ class PrinterManager {
|
||||
online: allPrinters.filter(p => ['online', 'idle'].includes(p.status)).length,
|
||||
offline: allPrinters.filter(p => p.status === 'offline').length,
|
||||
printing: allPrinters.filter(p => p.status === 'printing').length
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
// Animate counter updates
|
||||
this.animateCounter('total-count', stats.total);
|
||||
this.animateCounter('online-count', stats.online);
|
||||
@ -1694,7 +1694,7 @@ class PrinterManager {
|
||||
const locations = [...new Set(allPrinters.map(p => p.location).filter(Boolean))];
|
||||
locationFilter.innerHTML = '<option value="">Alle Standorte</option>' +
|
||||
locations.map(location => `<option value="${location}">${location}</option>`).join('');
|
||||
|
||||
|
||||
// Populate model filter
|
||||
const modelFilter = document.getElementById('filterModel');
|
||||
const models = [...new Set(allPrinters.map(p => p.model).filter(Boolean))];
|
||||
@ -1711,8 +1711,8 @@ class PrinterManager {
|
||||
const now = new Date();
|
||||
document.getElementById('last-update-time').textContent = now.toLocaleTimeString('de-DE');
|
||||
document.getElementById('last-update').textContent = `Letzte Aktualisierung: ${now.toLocaleTimeString('de-DE')}`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
formatDuration(minutes) {
|
||||
const hours = Math.floor(minutes / 60);
|
||||
@ -1778,13 +1778,13 @@ class PrinterManager {
|
||||
closeModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
const content = modal.querySelector('.mercedes-modal, .dashboard-card');
|
||||
|
||||
|
||||
content.classList.remove('scale-100', 'opacity-100');
|
||||
content.classList.add('scale-95', 'opacity-0');
|
||||
setTimeout(() => {
|
||||
modal.classList.add('hidden');
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize Printer Manager
|
||||
@ -1839,7 +1839,7 @@ function toggleGridView(view) {
|
||||
currentGridView = view;
|
||||
const gridBtn = document.getElementById('grid-view-btn');
|
||||
const listBtn = document.getElementById('list-view-btn');
|
||||
|
||||
|
||||
if (view === 'grid') {
|
||||
gridBtn.classList.add('bg-mercedes-blue', 'text-white');
|
||||
gridBtn.classList.remove('text-mercedes-gray', 'hover:bg-mercedes-silver');
|
||||
@ -1850,7 +1850,7 @@ function toggleGridView(view) {
|
||||
listBtn.classList.remove('text-mercedes-gray', 'hover:bg-mercedes-silver');
|
||||
gridBtn.classList.remove('bg-mercedes-blue', 'text-white');
|
||||
gridBtn.classList.add('text-mercedes-gray', 'hover:bg-mercedes-silver');
|
||||
}
|
||||
}
|
||||
|
||||
printerManager.displayPrinters();
|
||||
}
|
||||
@ -1878,6 +1878,6 @@ function closePrinterModal() {
|
||||
|
||||
function closePrinterDetailsModal() {
|
||||
printerManager.closeModal('printerDetailsModal');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
@ -262,7 +262,7 @@
|
||||
for (let [key, value] of formData.entries()) {
|
||||
if (key !== 'avatar') { // Don't store file data
|
||||
originalFormData[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user