"Refactor frontend templates and styles"

This commit is contained in:
Till Tomczak 2025-05-29 17:41:30 +02:00
parent b7062887b9
commit 06119be88b
8 changed files with 1752 additions and 1442 deletions

View File

@ -4298,26 +4298,6 @@ if __name__ == "__main__":
atexit.register(cleanup_queue_manager) 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: except Exception as e:
app_logger.error(f"❌ Fehler beim Starten des Queue-Managers: {str(e)}") app_logger.error(f"❌ Fehler beim Starten des Queue-Managers: {str(e)}")
else: else:
@ -4398,183 +4378,491 @@ if __name__ == "__main__":
pass pass
sys.exit(1) sys.exit(1)
def force_load_all_printers(): # ===== NEUE ADMIN API-ROUTEN FÜR BUTTON-FUNKTIONALITÄTEN =====
"""
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() @app.route('/api/admin/maintenance/activate', methods=['POST'])
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
@app.route('/api/admin/printers/force-initialize', methods=['POST'])
@admin_required @admin_required
def force_initialize_printers(): def activate_maintenance_mode():
"""Startet eine robuste Drucker-Initialisierung manuell""" """Aktiviert den Wartungsmodus"""
try: 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 # Für Demo-Zwecke simulieren wir die Aktivierung
import threading app_logger.info("Wartungsmodus aktiviert durch Admin")
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()
return jsonify({ return jsonify({
"success": True, "success": True,
"message": "Drucker-Initialisierung gestartet", "message": "Wartungsmodus wurde aktiviert"
"info": "Die Initialisierung läuft im Hintergrund. Prüfen Sie die Logs für Details."
}) })
except Exception as e: 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({ return jsonify({
"success": False, "success": False,
"message": f"Fehler beim Starten der Initialisierung: {str(e)}" "error": "Fehler beim Aktivieren des Wartungsmodus"
}), 500 }), 500
@app.route('/api/admin/printers/update-all', methods=['POST']) @app.route('/api/admin/maintenance/deactivate', methods=['POST'])
@admin_required @admin_required
def update_all_printers(): def deactivate_maintenance_mode():
"""Aktualisiert den Status aller Drucker""" """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: try:
db_session = get_db_session() 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: # Drucker-Statistiken
if printer.plug_ip: total_printers = db_session.query(func.count(Printer.id)).scalar() or 0
try: online_printers = db_session.query(func.count(Printer.id)).filter(
status, active = check_printer_status(printer.plug_ip) Printer.status.in_(['online', 'idle'])
old_status = printer.status ).scalar() or 0
printer.update_status(status, active) # Job-Statistiken
active_jobs = db_session.query(func.count(Job.id)).filter(
Job.status == 'running'
).scalar() or 0
updated_printers.append({ queued_jobs = db_session.query(func.count(Job.id)).filter(
"id": printer.id, Job.status == 'queued'
"name": printer.name, ).scalar() or 0
"old_status": old_status,
"new_status": status,
"active": active
})
except Exception as e: # Erfolgsrate berechnen
printers_logger.warning(f"Fehler beim Aktualisieren von Drucker {printer.name}: {str(e)}") 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() db_session.close()
app_logger.info(f"Status von {len(updated_printers)} Druckern aktualisiert")
return jsonify({ return jsonify({
"success": True, "success": True,
"message": f"Status von {len(updated_printers)} Druckern aktualisiert", "stats": {
"updated_printers": updated_printers "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: 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({ return jsonify({
"success": False, "success": False,
"message": f"Fehler beim Aktualisieren: {str(e)}" "error": "Fehler beim Laden der Statistiken"
}), 500 }), 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.

View File

@ -602,702 +602,3 @@
--shadow-dark: rgba(0, 0, 0, 0.5); --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

File diff suppressed because one or more lines are too long

View File

@ -536,7 +536,7 @@
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<!-- File Upload Section --> <!-- File Upload Section -->

View File

@ -860,7 +860,7 @@
<p class="text-mercedes-gray dark:text-slate-400 mb-8 leading-relaxed"> <p class="text-mercedes-gray dark:text-slate-400 mb-8 leading-relaxed">
Es wurden keine 3D-Drucker gefunden, die den aktuellen Filterkriterien entsprechen. 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. 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"> <div class="flex flex-col sm:flex-row gap-3 justify-center">
<button onclick="clearAllFilters()" class="btn-secondary"> <button onclick="clearAllFilters()" class="btn-secondary">
Filter zurücksetzen Filter zurücksetzen
@ -1270,7 +1270,7 @@ class PrinterManager {
} }
async loadPrinters() { async loadPrinters() {
try { try {
this.showLoadingState(); this.showLoadingState();
const response = await fetch('/api/printers'); const response = await fetch('/api/printers');
@ -1634,7 +1634,7 @@ class PrinterManager {
online: allPrinters.filter(p => ['online', 'idle'].includes(p.status)).length, online: allPrinters.filter(p => ['online', 'idle'].includes(p.status)).length,
offline: allPrinters.filter(p => p.status === 'offline').length, offline: allPrinters.filter(p => p.status === 'offline').length,
printing: allPrinters.filter(p => p.status === 'printing').length printing: allPrinters.filter(p => p.status === 'printing').length
}; };
// Animate counter updates // Animate counter updates
this.animateCounter('total-count', stats.total); this.animateCounter('total-count', stats.total);
@ -1711,7 +1711,7 @@ class PrinterManager {
const now = new Date(); const now = new Date();
document.getElementById('last-update-time').textContent = now.toLocaleTimeString('de-DE'); document.getElementById('last-update-time').textContent = now.toLocaleTimeString('de-DE');
document.getElementById('last-update').textContent = `Letzte Aktualisierung: ${now.toLocaleTimeString('de-DE')}`; document.getElementById('last-update').textContent = `Letzte Aktualisierung: ${now.toLocaleTimeString('de-DE')}`;
} }
// Utility functions // Utility functions
formatDuration(minutes) { formatDuration(minutes) {
@ -1784,7 +1784,7 @@ class PrinterManager {
setTimeout(() => { setTimeout(() => {
modal.classList.add('hidden'); modal.classList.add('hidden');
}, 200); }, 200);
} }
} }
// Initialize Printer Manager // Initialize Printer Manager
@ -1850,7 +1850,7 @@ function toggleGridView(view) {
listBtn.classList.remove('text-mercedes-gray', 'hover:bg-mercedes-silver'); listBtn.classList.remove('text-mercedes-gray', 'hover:bg-mercedes-silver');
gridBtn.classList.remove('bg-mercedes-blue', 'text-white'); gridBtn.classList.remove('bg-mercedes-blue', 'text-white');
gridBtn.classList.add('text-mercedes-gray', 'hover:bg-mercedes-silver'); gridBtn.classList.add('text-mercedes-gray', 'hover:bg-mercedes-silver');
} }
printerManager.displayPrinters(); printerManager.displayPrinters();
} }
@ -1878,6 +1878,6 @@ function closePrinterModal() {
function closePrinterDetailsModal() { function closePrinterDetailsModal() {
printerManager.closeModal('printerDetailsModal'); printerManager.closeModal('printerDetailsModal');
} }
</script> </script>
{% endblock %} {% endblock %}

View File

@ -262,7 +262,7 @@
for (let [key, value] of formData.entries()) { for (let [key, value] of formData.entries()) {
if (key !== 'avatar') { // Don't store file data if (key !== 'avatar') { // Don't store file data
originalFormData[key] = value; originalFormData[key] = value;
} }
} }
} }