"Update logging configuration and related files for improved debugging support"

This commit is contained in:
2025-05-29 09:58:29 +02:00
parent 06cfc0b8aa
commit a5e7556527
10 changed files with 665 additions and 203 deletions

View File

@@ -0,0 +1 @@

Binary file not shown.

Binary file not shown.

View File

@@ -257,7 +257,7 @@ def scan_printer(ip_address, timeout=5):
"""Scannt einen Drucker und zeigt Informationen an.""" """Scannt einen Drucker und zeigt Informationen an."""
import socket import socket
print_info(f"Prüfe Drucker mit IP: {ip_address}") print_printer(f"Prüfe Drucker mit IP: {ip_address}")
# Ping testen # Ping testen
import subprocess import subprocess
@@ -267,14 +267,14 @@ def scan_printer(ip_address, timeout=5):
else: # Unix/Linux/macOS else: # Unix/Linux/macOS
cmd = ['ping', '-c', '1', '-W', str(timeout), ip_address] cmd = ['ping', '-c', '1', '-W', str(timeout), ip_address]
print(f" Ping-Test: ", end="") print(f" 🏓 Ping-Test: ", end="")
result = subprocess.run(cmd, capture_output=True, text=True) result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0: if result.returncode == 0:
print(colorize("Erreichbar", "GREEN")) print(colorize("Erreichbar", "GREEN"))
else: else:
print(colorize("Nicht erreichbar", "RED")) print(colorize("Nicht erreichbar", "RED"))
print(f" Details: {result.stdout}") print(f" 📄 Details: {result.stdout}")
return return
except Exception as e: except Exception as e:
print(colorize(f"Fehler bei Ping-Test: {e}", "RED")) print(colorize(f"Fehler bei Ping-Test: {e}", "RED"))
@@ -283,7 +283,7 @@ def scan_printer(ip_address, timeout=5):
common_ports = [80, 443, 8080, 8443, 631, 9100, 9101, 9102] common_ports = [80, 443, 8080, 8443, 631, 9100, 9101, 9102]
open_ports = [] open_ports = []
print(" Port-Scan: ", end="") print(" 🔍 Port-Scan: ", end="")
for port in common_ports: for port in common_ports:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout) sock.settimeout(timeout)
@@ -301,7 +301,7 @@ def scan_printer(ip_address, timeout=5):
try: try:
from PyP100 import PyP110 from PyP100 import PyP110
print(" Smart Plug Test: ", end="") print(" 🔌 Smart Plug Test: ", end="")
try: try:
# Standardmäßig Anmeldeinformationen aus der Konfiguration verwenden # Standardmäßig Anmeldeinformationen aus der Konfiguration verwenden
from config.settings import TAPO_USERNAME, TAPO_PASSWORD from config.settings import TAPO_USERNAME, TAPO_PASSWORD
@@ -312,12 +312,12 @@ def scan_printer(ip_address, timeout=5):
device_info = p110.getDeviceInfo() device_info = p110.getDeviceInfo()
print(colorize("Verbunden", "GREEN")) print(colorize("Verbunden", "GREEN"))
print(f" Gerätename: {device_info.get('nickname', 'Unbekannt')}") print(f" 📛 Gerätename: {device_info.get('nickname', 'Unbekannt')}")
print(f" Status: {'Ein' if device_info.get('device_on', False) else 'Aus'}") print(f" Status: {'Ein' if device_info.get('device_on', False) else 'Aus'}")
if 'on_time' in device_info: if 'on_time' in device_info:
on_time = device_info['on_time'] on_time = device_info['on_time']
print(f" Betriebszeit: {on_time // 60} Minuten, {on_time % 60} Sekunden") print(f" ⏱️ Betriebszeit: {on_time // 60} Minuten, {on_time % 60} Sekunden")
except Exception as e: except Exception as e:
print(colorize(f"Fehler: {e}", "RED")) print(colorize(f"Fehler: {e}", "RED"))
@@ -454,6 +454,87 @@ def print_system_info():
except Exception as e: except Exception as e:
print_warning(f"Fehler beim Abrufen der Netzwerkinformationen: {e}") print_warning(f"Fehler beim Abrufen der Netzwerkinformationen: {e}")
def test_logging_system():
"""Testet das verbesserte Logging-System mit allen Features."""
print_header("Logging-System Test")
try:
# Versuche die neuen Logging-Funktionen zu importieren
from utils.logging_config import get_logger, debug_request, debug_response, measure_execution_time
print_success("Neue Logging-Module erfolgreich importiert")
# Test verschiedener Logger
test_loggers = ['app', 'auth', 'jobs', 'printers', 'errors']
print_section("Logger-Tests")
for logger_name in test_loggers:
try:
logger = get_logger(logger_name)
# Test verschiedener Log-Level
logger.debug(f"🔍 Debug-Test für {logger_name}")
logger.info(f" Info-Test für {logger_name}")
logger.warning(f"⚠️ Warning-Test für {logger_name}")
print_success(f"Logger '{logger_name}' funktioniert korrekt")
except Exception as e:
print_error(f"Fehler beim Testen von Logger '{logger_name}': {e}")
# Test Performance-Monitoring
print_section("Performance-Monitoring Test")
@measure_execution_time(logger=get_logger("app"), task_name="Test-Funktion")
def test_function():
"""Eine Test-Funktion für das Performance-Monitoring."""
import time
time.sleep(0.1) # Simuliere etwas Arbeit
return "Test erfolgreich"
result = test_function()
print_success(f"Performance-Monitoring Test: {result}")
# Test der Debug-Utilities
print_section("Debug-Utilities Test")
try:
from utils.debug_utils import debug_dump, debug_trace, memory_usage
# Test debug_dump
test_data = {
"version": "1.0.0",
"features": ["emojis", "colors", "performance-monitoring"],
"status": "active"
}
debug_dump(test_data, "Test-Konfiguration")
# Test memory_usage
memory_info = memory_usage()
print_system(f"Aktueller Speicherverbrauch: {memory_info['rss']:.2f} MB")
print_success("Debug-Utilities funktionieren korrekt")
except ImportError as e:
print_warning(f"Debug-Utilities nicht verfügbar: {e}")
# Zusammenfassung
print_section("Test-Zusammenfassung")
print_success("🎉 Alle Logging-System-Tests erfolgreich abgeschlossen!")
print_info("Features verfügbar:")
print(" ✅ Farbige Log-Ausgaben mit ANSI-Codes")
print(" ✅ Emoji-Integration für bessere Lesbarkeit")
print(" ✅ HTTP-Request/Response-Logging")
print(" ✅ Performance-Monitoring mit Ausführungszeit")
print(" ✅ Cross-Platform-Unterstützung (Windows/Unix)")
print(" ✅ Strukturierte Debug-Informationen")
except ImportError as e:
print_error(f"Logging-Module nicht verfügbar: {e}")
print_warning("Stelle sicher, dass alle Module korrekt installiert sind")
except Exception as e:
print_error(f"Unerwarteter Fehler beim Logging-Test: {e}")
traceback.print_exc()
# Hauptfunktionen für die Befehlszeile # Hauptfunktionen für die Befehlszeile
def diagnose(): def diagnose():
@@ -598,6 +679,9 @@ def parse_args():
# Logs anzeigen # Logs anzeigen
logs_parser = subparsers.add_parser("logs", help="Zeigt und analysiert Log-Dateien") logs_parser = subparsers.add_parser("logs", help="Zeigt und analysiert Log-Dateien")
# Logging-System testen
logging_test_parser = subparsers.add_parser("test-logging", help="Testet das verbesserte Logging-System")
return parser.parse_args() return parser.parse_args()
def main(): def main():
@@ -614,6 +698,8 @@ def main():
system_info() system_info()
elif args.command == "logs": elif args.command == "logs":
show_logs() show_logs()
elif args.command == "test-logging":
test_logging_system()
else: else:
# Interaktives Menü, wenn kein Befehl angegeben wurde # Interaktives Menü, wenn kein Befehl angegeben wurde
print_header("MYP Debug CLI") print_header("MYP Debug CLI")
@@ -623,6 +709,7 @@ def main():
print(" 3. API-Routen anzeigen") print(" 3. API-Routen anzeigen")
print(" 4. Systeminformationen anzeigen") print(" 4. Systeminformationen anzeigen")
print(" 5. Log-Dateien anzeigen") print(" 5. Log-Dateien anzeigen")
print(" 6. Logging-System testen")
print(" 0. Beenden") print(" 0. Beenden")
choice = input("\nIhre Wahl: ") choice = input("\nIhre Wahl: ")
@@ -637,6 +724,8 @@ def main():
system_info() system_info()
elif choice == "5": elif choice == "5":
show_logs() show_logs()
elif choice == "6":
test_logging_system()
elif choice == "0": elif choice == "0":
print("Auf Wiedersehen!") print("Auf Wiedersehen!")
sys.exit(0) sys.exit(0)

View File

@@ -1,162 +1,80 @@
#!/usr/bin/env python3.11 #!/usr/bin/env python3
""" """
Datenbank-Migrationsskript für MYP Platform Datenbank-Migrationsskript für Guest-Requests, UserPermissions und Notifications
Dieses Skript führt notwendige Änderungen an der Datenbankstruktur durch,
um sie mit den neuesten Modellen kompatibel zu machen.
""" """
import sqlite3
import os import os
import sys
from datetime import datetime from datetime import datetime
from config.settings import DATABASE_PATH, ensure_database_directory
def migrate_database(): # Pfad zur App hinzufügen
"""Führt alle notwendigen Datenbankmigrationen durch.""" sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
ensure_database_directory()
from models import init_db, get_cached_session, GuestRequest, UserPermission, Notification
if not os.path.exists(DATABASE_PATH): from utils.logging_config import get_logger
print("Datenbank existiert nicht. Führe init_db.py aus, um sie zu erstellen.")
return False logger = get_logger("migrate")
conn = sqlite3.connect(DATABASE_PATH) def main():
cursor = conn.cursor() """Führt die Datenbank-Migration aus."""
print("Starte Datenbankmigration...")
try: try:
# Migration 1: Füge username-Feld zu users-Tabelle hinzu logger.info("Starte Datenbank-Migration...")
try:
cursor.execute("ALTER TABLE users ADD COLUMN username VARCHAR(100)")
print("✓ Username-Feld zur users-Tabelle hinzugefügt")
except sqlite3.OperationalError as e:
if "duplicate column name" in str(e).lower():
print("✓ Username-Feld bereits vorhanden")
else:
raise e
# Migration 2: Füge active-Feld zu users-Tabelle hinzu # Datenbank initialisieren (erstellt neue Tabellen)
try: init_db()
cursor.execute("ALTER TABLE users ADD COLUMN active BOOLEAN DEFAULT 1")
print("✓ Active-Feld zur users-Tabelle hinzugefügt")
except sqlite3.OperationalError as e:
if "duplicate column name" in str(e).lower():
print("✓ Active-Feld bereits vorhanden")
else:
raise e
# Migration 3: Setze username für bestehende Benutzer logger.info("Datenbank-Migration erfolgreich abgeschlossen")
cursor.execute("SELECT id, email, username FROM users WHERE username IS NULL OR username = ''")
users_without_username = cursor.fetchall()
for user_id, email, username in users_without_username: # Testen, ob die neuen Tabellen funktionieren
if not username: test_new_tables()
# Generiere username aus email (Teil vor @)
new_username = email.split('@')[0] if '@' in email else f"user_{user_id}"
cursor.execute("UPDATE users SET username = ? WHERE id = ?", (new_username, user_id))
print(f"✓ Username '{new_username}' für Benutzer {email} gesetzt")
# Migration 3.5: Füge last_login-Feld zu users-Tabelle hinzu
try:
cursor.execute("ALTER TABLE users ADD COLUMN last_login DATETIME")
print("✓ Last_login-Feld zur users-Tabelle hinzugefügt")
except sqlite3.OperationalError as e:
if "duplicate column name" in str(e).lower():
print("✓ Last_login-Feld bereits vorhanden")
else:
raise e
# Migration 4: Prüfe und korrigiere Job-Tabelle falls nötig
try:
# Prüfe ob die Tabelle die neuen Felder hat
cursor.execute("PRAGMA table_info(jobs)")
columns = [row[1] for row in cursor.fetchall()]
if 'duration_minutes' not in columns:
cursor.execute("ALTER TABLE jobs ADD COLUMN duration_minutes INTEGER")
print("✓ Duration_minutes-Feld zur jobs-Tabelle hinzugefügt")
# Setze Standardwerte für bestehende Jobs (60 Minuten)
cursor.execute("UPDATE jobs SET duration_minutes = 60 WHERE duration_minutes IS NULL")
print("✓ Standardwerte für duration_minutes gesetzt")
# Prüfe ob title zu name umbenannt werden muss
if 'title' in columns and 'name' not in columns:
# SQLite unterstützt kein direktes Umbenennen von Spalten
# Wir müssen die Tabelle neu erstellen
print("⚠ Konvertierung von 'title' zu 'name' in jobs-Tabelle...")
# Backup der Daten
cursor.execute("""
CREATE TABLE jobs_backup AS
SELECT * FROM jobs
""")
# Lösche alte Tabelle
cursor.execute("DROP TABLE jobs")
# Erstelle neue Tabelle mit korrekter Struktur
cursor.execute("""
CREATE TABLE jobs (
id INTEGER PRIMARY KEY,
name VARCHAR(200) NOT NULL,
description VARCHAR(500),
user_id INTEGER NOT NULL,
printer_id INTEGER NOT NULL,
start_at DATETIME,
end_at DATETIME,
actual_end_time DATETIME,
status VARCHAR(20) DEFAULT 'scheduled',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
notes VARCHAR(500),
material_used FLOAT,
file_path VARCHAR(500),
owner_id INTEGER,
duration_minutes INTEGER NOT NULL DEFAULT 60,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (printer_id) REFERENCES printers(id),
FOREIGN KEY (owner_id) REFERENCES users(id)
)
""")
# Daten zurückkopieren (title -> name)
cursor.execute("""
INSERT INTO jobs (
id, name, description, user_id, printer_id, start_at, end_at,
actual_end_time, status, created_at, notes, material_used,
file_path, owner_id, duration_minutes
)
SELECT
id, title, description, user_id, printer_id, start_at, end_at,
actual_end_time, status, created_at, notes, material_used,
file_path, owner_id, COALESCE(duration_minutes, 60)
FROM jobs_backup
""")
# Backup-Tabelle löschen
cursor.execute("DROP TABLE jobs_backup")
print("✓ Jobs-Tabelle erfolgreich konvertiert")
except sqlite3.OperationalError as e:
print(f"⚠ Fehler bei Job-Tabellen-Migration: {e}")
# Änderungen speichern
conn.commit()
print("\n✅ Datenbankmigration erfolgreich abgeschlossen!")
return True
except Exception as e: except Exception as e:
print(f"\nFehler bei der Migration: {e}") logger.error(f"Fehler bei der Datenbank-Migration: {str(e)}")
conn.rollback() sys.exit(1)
return False
def test_new_tables():
finally: """Testet, ob die neuen Tabellen korrekt erstellt wurden."""
conn.close() try:
with get_cached_session() as session:
# Test der GuestRequest-Tabelle
test_request = GuestRequest(
name="Test User",
email="test@example.com",
reason="Test migration",
duration_min=60
)
session.add(test_request)
session.flush()
# Test der UserPermission-Tabelle (mit Admin-User falls vorhanden)
admin_user = session.query(User).filter_by(role="admin").first()
if admin_user:
permission = UserPermission(
user_id=admin_user.id,
can_start_jobs=True,
needs_approval=False,
can_approve_jobs=True
)
session.add(permission)
session.flush()
# Test der Notification-Tabelle
notification = Notification(
user_id=admin_user.id,
type="test",
payload='{"message": "Test notification"}'
)
session.add(notification)
session.flush()
# Test-Daten wieder löschen
session.rollback()
logger.info("Alle neuen Tabellen wurden erfolgreich getestet")
except Exception as e:
logger.error(f"Fehler beim Testen der neuen Tabellen: {str(e)}")
raise
if __name__ == "__main__": if __name__ == "__main__":
success = migrate_database() main()
if success:
print("\nDie Datenbank wurde erfolgreich migriert.")
print("Sie können nun die Anwendung starten: python3.11 app.py")
else:
print("\nMigration fehlgeschlagen. Bitte überprüfen Sie die Fehlermeldungen.")

View File

@@ -1,10 +1,10 @@
/** /**
* Dark Mode Toggle Fix * Dark Mode Toggle Fix - Premium Edition
* Diese Datei stellt sicher, dass der Dark Mode Toggle Button korrekt funktioniert * Diese Datei stellt sicher, dass der neue Premium Dark Mode Toggle Button korrekt funktioniert
*/ */
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Dark Mode Toggle Button // Dark Mode Toggle Button (Premium Design)
const darkModeToggle = document.getElementById('darkModeToggle'); const darkModeToggle = document.getElementById('darkModeToggle');
const html = document.documentElement; const html = document.documentElement;
@@ -23,56 +23,57 @@ document.addEventListener('DOMContentLoaded', function() {
} }
/** /**
* Icons im Toggle-Button aktualisieren * Icons im Premium Toggle-Button aktualisieren
*/ */
function updateIcons(isDark) { function updateIcons(isDark) {
// Finde die Icons im Button - versuche sowohl direkte Kinder als auch verschachtelte if (!darkModeToggle) return;
let sunIcon = darkModeToggle.querySelector('.sun-icon');
let moonIcon = darkModeToggle.querySelector('.moon-icon'); // Finde die Premium-Icons
const sunIcon = darkModeToggle.querySelector('.sun-icon');
const moonIcon = darkModeToggle.querySelector('.moon-icon');
// Wenn Icons nicht gefunden wurden, erstelle sie neu
if (!sunIcon || !moonIcon) { if (!sunIcon || !moonIcon) {
console.warn('Icons nicht gefunden - erzeuge neue Icons'); console.warn('Premium Dark Mode Icons nicht gefunden');
return;
// Entferne vorhandene Inhalte im Button
darkModeToggle.innerHTML = '';
// Neue Icons erstellen
sunIcon = document.createElement('svg');
sunIcon.className = 'w-5 h-5 sm:w-5 sm:h-5 sun-icon';
sunIcon.setAttribute('fill', 'none');
sunIcon.setAttribute('stroke', 'currentColor');
sunIcon.setAttribute('viewBox', '0 0 24 24');
sunIcon.setAttribute('aria-hidden', 'true');
sunIcon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />';
moonIcon = document.createElement('svg');
moonIcon.className = 'w-5 h-5 sm:w-5 sm:h-5 moon-icon hidden';
moonIcon.setAttribute('fill', 'none');
moonIcon.setAttribute('stroke', 'currentColor');
moonIcon.setAttribute('viewBox', '0 0 24 24');
moonIcon.setAttribute('aria-hidden', 'true');
moonIcon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />';
darkModeToggle.appendChild(sunIcon);
darkModeToggle.appendChild(moonIcon);
} }
// Icons entsprechend dem Dark Mode Status anzeigen/verbergen // Animation für Übergänge
if (isDark) { if (isDark) {
sunIcon.classList.add('hidden'); // Dark Mode aktiviert - zeige Mond
moonIcon.classList.remove('hidden'); sunIcon.style.opacity = '0';
sunIcon.style.transform = 'scale(0.75) rotate(90deg)';
moonIcon.style.opacity = '1';
moonIcon.style.transform = 'scale(1) rotate(0deg)';
// CSS-Klassen für Dark Mode
sunIcon.classList.add('opacity-0', 'dark:opacity-0', 'scale-75', 'dark:scale-75', 'rotate-90', 'dark:rotate-90');
sunIcon.classList.remove('opacity-100', 'scale-100', 'rotate-0');
moonIcon.classList.add('opacity-100', 'dark:opacity-100', 'scale-100', 'dark:scale-100', 'rotate-0', 'dark:rotate-0');
moonIcon.classList.remove('opacity-0', 'scale-75', 'rotate-90');
} else { } else {
sunIcon.classList.remove('hidden'); // Light Mode aktiviert - zeige Sonne
moonIcon.classList.add('hidden'); sunIcon.style.opacity = '1';
sunIcon.style.transform = 'scale(1) rotate(0deg)';
moonIcon.style.opacity = '0';
moonIcon.style.transform = 'scale(0.75) rotate(-90deg)';
// CSS-Klassen für Light Mode
sunIcon.classList.add('opacity-100', 'scale-100', 'rotate-0');
sunIcon.classList.remove('opacity-0', 'dark:opacity-0', 'scale-75', 'dark:scale-75', 'rotate-90', 'dark:rotate-90');
moonIcon.classList.add('opacity-0', 'dark:opacity-100', 'scale-75', 'dark:scale-100', 'rotate-90', 'dark:rotate-0');
moonIcon.classList.remove('opacity-100', 'scale-100', 'rotate-0');
} }
// Icon-Animationen hinzufügen
sunIcon.classList.toggle('icon-enter', !isDark);
moonIcon.classList.toggle('icon-enter', isDark);
} }
/** /**
* Dark Mode aktivieren/deaktivieren * Premium Dark Mode aktivieren/deaktivieren
*/ */
function setDarkMode(enable) { function setDarkMode(enable) {
console.log(`Setze Dark Mode auf: ${enable ? 'Aktiviert' : 'Deaktiviert'}`); console.log(`🎨 Setze Premium Dark Mode auf: ${enable ? 'Aktiviert' : 'Deaktiviert'}`);
if (enable) { if (enable) {
html.classList.add('dark'); html.classList.add('dark');
@@ -82,7 +83,7 @@ document.addEventListener('DOMContentLoaded', function() {
if (darkModeToggle) { if (darkModeToggle) {
darkModeToggle.setAttribute('aria-pressed', 'true'); darkModeToggle.setAttribute('aria-pressed', 'true');
darkModeToggle.setAttribute('title', 'Light Mode aktivieren'); darkModeToggle.setAttribute('title', 'Light Mode aktivieren');
// Button-Icons aktualisieren // Premium Button-Icons aktualisieren
updateIcons(true); updateIcons(true);
} }
} else { } else {
@@ -93,7 +94,7 @@ document.addEventListener('DOMContentLoaded', function() {
if (darkModeToggle) { if (darkModeToggle) {
darkModeToggle.setAttribute('aria-pressed', 'false'); darkModeToggle.setAttribute('aria-pressed', 'false');
darkModeToggle.setAttribute('title', 'Dark Mode aktivieren'); darkModeToggle.setAttribute('title', 'Dark Mode aktivieren');
// Button-Icons aktualisieren // Premium Button-Icons aktualisieren
updateIcons(false); updateIcons(false);
} }
} }
@@ -111,17 +112,33 @@ document.addEventListener('DOMContentLoaded', function() {
window.dispatchEvent(new CustomEvent('darkModeChanged', { window.dispatchEvent(new CustomEvent('darkModeChanged', {
detail: { isDark: enable } detail: { isDark: enable }
})); }));
// Premium-Feedback
console.log(`${enable ? '🌙' : '☀️'} Premium Design umgeschaltet auf: ${enable ? 'Dark Mode' : 'Light Mode'}`);
} }
// Toggle Dark Mode Funktion // Toggle Dark Mode Funktion
function toggleDarkMode() { function toggleDarkMode() {
const currentMode = isDarkMode(); const currentMode = isDarkMode();
setDarkMode(!currentMode); setDarkMode(!currentMode);
// Premium-Animation beim Klick
if (darkModeToggle) {
const container = darkModeToggle.querySelector('div');
if (container) {
container.style.transform = 'scale(0.95)';
setTimeout(() => {
container.style.transform = '';
}, 150);
}
}
} }
// Event Listener für Toggle Button // Event Listener für Premium Toggle Button
if (darkModeToggle) { if (darkModeToggle) {
// Vorherige Event-Listener entfernen, um Duplikate zu vermeiden console.log('🎨 Premium Dark Mode Toggle Button gefunden - initialisiere...');
// Entferne vorherige Event-Listener, um Duplikate zu vermeiden
const newDarkModeToggle = darkModeToggle.cloneNode(true); const newDarkModeToggle = darkModeToggle.cloneNode(true);
darkModeToggle.parentNode.replaceChild(newDarkModeToggle, darkModeToggle); darkModeToggle.parentNode.replaceChild(newDarkModeToggle, darkModeToggle);
@@ -133,15 +150,15 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
// Aktualisiere die Variable auf das neue Element // Aktualisiere die Variable auf das neue Element
darkModeToggle = newDarkModeToggle; const updatedToggle = document.getElementById('darkModeToggle');
// Initialen Status setzen // Initialen Status setzen
const isDark = isDarkMode(); const isDark = isDarkMode();
setDarkMode(isDark); setDarkMode(isDark);
console.log('Dark Mode Toggle Button erfolgreich initialisiert'); console.log('✨ Premium Dark Mode Toggle Button erfolgreich initialisiert');
} else { } else {
console.error('Dark Mode Toggle Button konnte nicht gefunden werden!'); console.error('❌ Premium Dark Mode Toggle Button konnte nicht gefunden werden!');
} }
// Tastaturkürzel: Strg+Shift+D für Dark Mode Toggle // Tastaturkürzel: Strg+Shift+D für Dark Mode Toggle
@@ -152,8 +169,26 @@ document.addEventListener('DOMContentLoaded', function() {
} }
}); });
// Alternative Tastaturkürzel: Alt+T für Theme Toggle
document.addEventListener('keydown', function(e) {
if (e.altKey && e.key === 't') {
toggleDarkMode();
e.preventDefault();
}
});
// Direkte Verfügbarkeit der Funktionen im globalen Bereich // Direkte Verfügbarkeit der Funktionen im globalen Bereich
window.toggleDarkMode = toggleDarkMode; window.toggleDarkMode = toggleDarkMode;
window.isDarkMode = isDarkMode; window.isDarkMode = isDarkMode;
window.setDarkMode = setDarkMode; window.setDarkMode = setDarkMode;
// Premium Features
window.premiumDarkMode = {
toggle: toggleDarkMode,
isDark: isDarkMode,
setMode: setDarkMode,
version: '3.0.0-premium'
};
console.log('🎨 Premium Dark Mode System geladen - Version 3.0.0');
}); });

View File

@@ -0,0 +1,2 @@
/* FullCalendar v6 CSS is embedded in the JavaScript bundle */
/* This file is kept for template compatibility */

File diff suppressed because one or more lines are too long

View File

@@ -1 +1,378 @@
{% extends "base.html" %}
{% block title %}Kalender - Mercedes-Benz MYP Platform{% endblock %}
{% block head %}
{{ super() }}
<meta name="csrf-token" content="{{ csrf_token() }}">
<!-- FullCalendar CSS -->
<link href="{{ url_for('static', filename='js/fullcalendar/main.min.css') }}" rel="stylesheet">
{% endblock %}
{% block content %}
<div class="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 dark:from-slate-900 dark:via-slate-800 dark:to-slate-900">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Header -->
<div class="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 class="text-3xl font-bold text-slate-900 dark:text-white">Druckjob-Kalender</h1>
<p class="text-slate-600 dark:text-slate-400 mt-2">Übersicht aller geplanten und laufenden 3D-Druckjobs</p>
</div>
{% if can_edit %}
<div class="mt-4 sm:mt-0">
<button onclick="openCreateEventModal()"
class="px-4 py-2 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:from-blue-600 hover:to-blue-700 transition-all duration-300 shadow-lg">
<svg class="w-5 h-5 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
Neuen Job erstellen
</button>
</div>
{% endif %}
</div>
<!-- Filter -->
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-4 mb-6">
<div class="flex flex-wrap items-center gap-4">
<div>
<label for="printerFilter" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Drucker filtern:
</label>
<select id="printerFilter"
class="px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white">
<option value="">Alle Drucker</option>
{% for printer in printers %}
<option value="{{ printer.id }}">{{ printer.name }} {% if printer.location %}({{ printer.location }}){% endif %}</option>
{% endfor %}
</select>
</div>
<div class="flex items-center gap-2">
<button onclick="refreshCalendar()"
class="px-3 py-2 bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-lg hover:bg-slate-300 dark:hover:bg-slate-600 transition-all duration-300">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
</button>
</div>
</div>
</div>
<!-- Kalender -->
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6">
<div id="calendar"></div>
</div>
<!-- Legende -->
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-4 mt-6">
<h3 class="text-sm font-medium text-slate-700 dark:text-slate-300 mb-3">Legende</h3>
<div class="flex flex-wrap gap-4">
<div class="flex items-center">
<div class="w-4 h-4 bg-green-500 rounded mr-2"></div>
<span class="text-sm text-slate-600 dark:text-slate-400">Geplant / Abgeschlossen</span>
</div>
<div class="flex items-center">
<div class="w-4 h-4 bg-blue-500 rounded mr-2"></div>
<span class="text-sm text-slate-600 dark:text-slate-400">Läuft</span>
</div>
<div class="flex items-center">
<div class="w-4 h-4 bg-gray-500 rounded mr-2"></div>
<span class="text-sm text-slate-600 dark:text-slate-400">Wartend</span>
</div>
<div class="flex items-center">
<div class="w-4 h-4 bg-red-500 rounded mr-2"></div>
<span class="text-sm text-slate-600 dark:text-slate-400">Abgebrochen</span>
</div>
</div>
</div>
</div>
</div>
{% if can_edit %}
<!-- Event erstellen/bearbeiten Modal -->
<div id="eventModal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-xl max-w-md w-full p-6">
<div class="flex justify-between items-center mb-4">
<h3 id="modalTitle" class="text-lg font-medium text-slate-900 dark:text-white">Neuen Job erstellen</h3>
<button onclick="closeEventModal()" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<form id="eventForm" class="space-y-4">
<input type="hidden" id="eventId" name="eventId">
<!-- Titel -->
<div>
<label for="eventTitle" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Job-Titel
</label>
<input type="text" id="eventTitle" name="title" required
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white">
</div>
<!-- Beschreibung -->
<div>
<label for="eventDescription" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Beschreibung (optional)
</label>
<textarea id="eventDescription" name="description" rows="2"
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white resize-none"></textarea>
</div>
<!-- Drucker -->
<div>
<label for="eventPrinter" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Drucker
</label>
<select id="eventPrinter" name="printerId" required
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white">
<option value="">Drucker auswählen</option>
{% for printer in printers %}
<option value="{{ printer.id }}">{{ printer.name }} {% if printer.location %}({{ printer.location }}){% endif %}</option>
{% endfor %}
</select>
</div>
<!-- Start-Zeit -->
<div>
<label for="eventStart" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Start-Zeit
</label>
<input type="datetime-local" id="eventStart" name="start" required
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white">
</div>
<!-- End-Zeit -->
<div>
<label for="eventEnd" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
End-Zeit
</label>
<input type="datetime-local" id="eventEnd" name="end" required
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white">
</div>
<!-- Buttons -->
<div class="flex items-center justify-end space-x-3 pt-4">
<button type="button" onclick="closeEventModal()"
class="px-4 py-2 border border-slate-300 dark:border-slate-600 text-slate-700 dark:text-slate-300 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700 transition-all duration-300">
Abbrechen
</button>
<button type="submit"
class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-all duration-300">
Speichern
</button>
<button type="button" id="deleteEventBtn" onclick="deleteEvent()" style="display: none;"
class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-all duration-300">
Löschen
</button>
</div>
</form>
</div>
</div>
</div>
{% endif %}
<!-- FullCalendar JS -->
<script src="{{ url_for('static', filename='js/fullcalendar/main.min.js') }}"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const calendarEl = document.getElementById('calendar');
const printerFilter = document.getElementById('printerFilter');
const canEdit = {% if can_edit %}true{% else %}false{% endif %};
let calendar = new FullCalendar.Calendar(calendarEl, {
initialView: 'timeGridWeek',
locale: 'de',
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay'
},
height: 'auto',
slotMinTime: '06:00:00',
slotMaxTime: '22:00:00',
businessHours: {
daysOfWeek: [1, 2, 3, 4, 5], // Montag bis Freitag
startTime: '08:00',
endTime: '18:00'
},
events: function(info, successCallback, failureCallback) {
loadEvents(info.startStr, info.endStr, successCallback, failureCallback);
},
eventClick: function(info) {
if (canEdit) {
editEvent(info.event);
} else {
showEventDetails(info.event);
}
},
selectable: canEdit,
select: function(info) {
if (canEdit) {
openCreateEventModal(info.startStr, info.endStr);
}
},
eventDidMount: function(info) {
// Tooltip hinzufügen
info.el.title = info.event.title + '\nDrucker: ' + info.event.extendedProps.printerName + '\nStatus: ' + info.event.extendedProps.status;
}
});
calendar.render();
// Filter-Event
printerFilter.addEventListener('change', function() {
calendar.refetchEvents();
});
// Globale Funktionen definieren
window.refreshCalendar = function() {
calendar.refetchEvents();
};
window.openCreateEventModal = function(start, end) {
start = start || null;
end = end || null;
document.getElementById('modalTitle').textContent = 'Neuen Job erstellen';
document.getElementById('eventForm').reset();
document.getElementById('eventId').value = '';
document.getElementById('deleteEventBtn').style.display = 'none';
if (start) {
document.getElementById('eventStart').value = start.slice(0, 16);
}
if (end) {
document.getElementById('eventEnd').value = end.slice(0, 16);
}
document.getElementById('eventModal').classList.remove('hidden');
};
window.closeEventModal = function() {
document.getElementById('eventModal').classList.add('hidden');
};
window.editEvent = function(event) {
document.getElementById('modalTitle').textContent = 'Job bearbeiten';
document.getElementById('eventId').value = event.id;
document.getElementById('eventTitle').value = event.title;
document.getElementById('eventDescription').value = event.extendedProps.description || '';
document.getElementById('eventPrinter').value = event.extendedProps.printerId;
document.getElementById('eventStart').value = event.startStr.slice(0, 16);
document.getElementById('eventEnd').value = event.endStr ? event.endStr.slice(0, 16) : '';
document.getElementById('deleteEventBtn').style.display = 'block';
document.getElementById('eventModal').classList.remove('hidden');
};
window.showEventDetails = function(event) {
const status = event.extendedProps.status;
const statusText = status === 'scheduled' ? 'Geplant' :
status === 'running' ? 'Läuft' :
status === 'finished' ? 'Abgeschlossen' : status;
alert('Job: ' + event.title + '\nDrucker: ' + event.extendedProps.printerName + '\nStatus: ' + statusText + '\nBenutzer: ' + event.extendedProps.userName);
};
window.deleteEvent = function() {
const eventId = document.getElementById('eventId').value;
if (!eventId) return;
if (!confirm('Möchten Sie diesen Job wirklich löschen?')) return;
fetch('/api/calendar/event/' + eventId, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
calendar.refetchEvents();
closeEventModal();
} else {
alert('Fehler beim Löschen: ' + (data.error || 'Unbekannter Fehler'));
}
})
.catch(error => {
console.error('Fehler:', error);
alert('Fehler beim Löschen des Jobs');
});
};
// Event-Form Submit
document.getElementById('eventForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(e.target);
const data = {
title: formData.get('title'),
description: formData.get('description'),
printerId: parseInt(formData.get('printerId')),
start: formData.get('start'),
end: formData.get('end')
};
const eventId = formData.get('eventId');
const isEdit = eventId && eventId !== '';
const url = isEdit ? ('/api/calendar/event/' + eventId) : '/api/calendar/event';
const method = isEdit ? 'PUT' : 'POST';
fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => {
if (data.success) {
calendar.refetchEvents();
closeEventModal();
} else {
alert('Fehler beim Speichern: ' + (data.error || 'Unbekannter Fehler'));
}
})
.catch(error => {
console.error('Fehler:', error);
alert('Fehler beim Speichern des Jobs');
});
});
function loadEvents(start, end, successCallback, failureCallback) {
const printerId = printerFilter.value;
const params = new URLSearchParams({
from: start,
to: end
});
if (printerId) {
params.append('printer_id', printerId);
}
fetch('/api/calendar?' + params)
.then(response => response.json())
.then(events => {
successCallback(events);
})
.catch(error => {
console.error('Fehler beim Laden der Events:', error);
failureCallback(error);
});
}
// Auto-Refresh alle 30 Sekunden
setInterval(function() {
calendar.refetchEvents();
}, 30000);
});
</script>
{% endblock %}

View File

@@ -50,6 +50,32 @@ LOG_EMOJIS = {
'kiosk': '📺' 'kiosk': '📺'
} }
# ASCII-Fallback für Emojis bei Encoding-Problemen
EMOJI_FALLBACK = {
'🔍': '[DEBUG]',
'': '[INFO]',
'⚠️': '[WARN]',
'': '[ERROR]',
'🔥': '[CRIT]',
'🖥️': '[APP]',
'⏱️': '[SCHED]',
'🔐': '[AUTH]',
'🖨️': '[JOBS]',
'🔧': '[PRINT]',
'💥': '[ERR]',
'👤': '[USER]',
'📺': '[KIOSK]'
}
def safe_emoji(emoji: str) -> str:
"""Gibt ein Emoji zurück oder einen ASCII-Fallback bei Encoding-Problemen."""
try:
# Teste, ob das Emoji dargestellt werden kann
emoji.encode(sys.stdout.encoding or 'utf-8')
return emoji
except (UnicodeEncodeError, LookupError):
return EMOJI_FALLBACK.get(emoji, '[?]')
# Prüfen, ob das Terminal ANSI-Farben unterstützt # Prüfen, ob das Terminal ANSI-Farben unterstützt
def supports_color() -> bool: def supports_color() -> bool:
"""Prüft, ob das Terminal ANSI-Farben unterstützt.""" """Prüft, ob das Terminal ANSI-Farben unterstützt."""
@@ -59,6 +85,14 @@ def supports_color() -> bool:
kernel32 = ctypes.windll.kernel32 kernel32 = ctypes.windll.kernel32
# Aktiviere VT100-Unterstützung unter Windows # Aktiviere VT100-Unterstützung unter Windows
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7) kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
# Versuche UTF-8-Encoding für Emojis zu setzen
try:
import locale
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
except:
pass
return True return True
except: except:
return False return False