"Update logging configuration and related files for improved debugging support"
This commit is contained in:
parent
06cfc0b8aa
commit
a5e7556527
1
backend/app/LOGGING_README.md
Normal file
1
backend/app/LOGGING_README.md
Normal file
@ -0,0 +1 @@
|
||||
|
Binary file not shown.
Binary file not shown.
@ -257,7 +257,7 @@ def scan_printer(ip_address, timeout=5):
|
||||
"""Scannt einen Drucker und zeigt Informationen an."""
|
||||
import socket
|
||||
|
||||
print_info(f"Prüfe Drucker mit IP: {ip_address}")
|
||||
print_printer(f"Prüfe Drucker mit IP: {ip_address}")
|
||||
|
||||
# Ping testen
|
||||
import subprocess
|
||||
@ -267,14 +267,14 @@ def scan_printer(ip_address, timeout=5):
|
||||
else: # Unix/Linux/macOS
|
||||
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)
|
||||
|
||||
if result.returncode == 0:
|
||||
print(colorize("Erreichbar", "GREEN"))
|
||||
else:
|
||||
print(colorize("Nicht erreichbar", "RED"))
|
||||
print(f" Details: {result.stdout}")
|
||||
print(f" 📄 Details: {result.stdout}")
|
||||
return
|
||||
except Exception as e:
|
||||
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]
|
||||
open_ports = []
|
||||
|
||||
print(" Port-Scan: ", end="")
|
||||
print(" 🔍 Port-Scan: ", end="")
|
||||
for port in common_ports:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(timeout)
|
||||
@ -301,7 +301,7 @@ def scan_printer(ip_address, timeout=5):
|
||||
try:
|
||||
from PyP100 import PyP110
|
||||
|
||||
print(" Smart Plug Test: ", end="")
|
||||
print(" 🔌 Smart Plug Test: ", end="")
|
||||
try:
|
||||
# Standardmäßig Anmeldeinformationen aus der Konfiguration verwenden
|
||||
from config.settings import TAPO_USERNAME, TAPO_PASSWORD
|
||||
@ -312,12 +312,12 @@ def scan_printer(ip_address, timeout=5):
|
||||
|
||||
device_info = p110.getDeviceInfo()
|
||||
print(colorize("Verbunden", "GREEN"))
|
||||
print(f" Gerätename: {device_info.get('nickname', 'Unbekannt')}")
|
||||
print(f" Status: {'Ein' if device_info.get('device_on', False) else 'Aus'}")
|
||||
print(f" 📛 Gerätename: {device_info.get('nickname', 'Unbekannt')}")
|
||||
print(f" ⚡ Status: {'Ein' if device_info.get('device_on', False) else 'Aus'}")
|
||||
|
||||
if 'on_time' in device_info:
|
||||
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:
|
||||
print(colorize(f"Fehler: {e}", "RED"))
|
||||
@ -454,6 +454,87 @@ def print_system_info():
|
||||
except Exception as 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
|
||||
|
||||
def diagnose():
|
||||
@ -598,6 +679,9 @@ def parse_args():
|
||||
# Logs anzeigen
|
||||
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()
|
||||
|
||||
def main():
|
||||
@ -614,6 +698,8 @@ def main():
|
||||
system_info()
|
||||
elif args.command == "logs":
|
||||
show_logs()
|
||||
elif args.command == "test-logging":
|
||||
test_logging_system()
|
||||
else:
|
||||
# Interaktives Menü, wenn kein Befehl angegeben wurde
|
||||
print_header("MYP Debug CLI")
|
||||
@ -623,6 +709,7 @@ def main():
|
||||
print(" 3. API-Routen anzeigen")
|
||||
print(" 4. Systeminformationen anzeigen")
|
||||
print(" 5. Log-Dateien anzeigen")
|
||||
print(" 6. Logging-System testen")
|
||||
print(" 0. Beenden")
|
||||
|
||||
choice = input("\nIhre Wahl: ")
|
||||
@ -637,6 +724,8 @@ def main():
|
||||
system_info()
|
||||
elif choice == "5":
|
||||
show_logs()
|
||||
elif choice == "6":
|
||||
test_logging_system()
|
||||
elif choice == "0":
|
||||
print("Auf Wiedersehen!")
|
||||
sys.exit(0)
|
||||
|
@ -1,162 +1,80 @@
|
||||
#!/usr/bin/env python3.11
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Datenbank-Migrationsskript für MYP Platform
|
||||
|
||||
Dieses Skript führt notwendige Änderungen an der Datenbankstruktur durch,
|
||||
um sie mit den neuesten Modellen kompatibel zu machen.
|
||||
Datenbank-Migrationsskript für Guest-Requests, UserPermissions und Notifications
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from config.settings import DATABASE_PATH, ensure_database_directory
|
||||
|
||||
def migrate_database():
|
||||
"""Führt alle notwendigen Datenbankmigrationen durch."""
|
||||
ensure_database_directory()
|
||||
|
||||
if not os.path.exists(DATABASE_PATH):
|
||||
print("Datenbank existiert nicht. Führe init_db.py aus, um sie zu erstellen.")
|
||||
return False
|
||||
|
||||
conn = sqlite3.connect(DATABASE_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("Starte Datenbankmigration...")
|
||||
|
||||
# Pfad zur App hinzufügen
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from models import init_db, get_cached_session, GuestRequest, UserPermission, Notification
|
||||
from utils.logging_config import get_logger
|
||||
|
||||
logger = get_logger("migrate")
|
||||
|
||||
def main():
|
||||
"""Führt die Datenbank-Migration aus."""
|
||||
try:
|
||||
# Migration 1: Füge username-Feld zu users-Tabelle hinzu
|
||||
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
|
||||
logger.info("Starte Datenbank-Migration...")
|
||||
|
||||
# Migration 2: Füge active-Feld zu users-Tabelle hinzu
|
||||
try:
|
||||
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
|
||||
# Datenbank initialisieren (erstellt neue Tabellen)
|
||||
init_db()
|
||||
|
||||
# Migration 3: Setze username für bestehende Benutzer
|
||||
cursor.execute("SELECT id, email, username FROM users WHERE username IS NULL OR username = ''")
|
||||
users_without_username = cursor.fetchall()
|
||||
logger.info("Datenbank-Migration erfolgreich abgeschlossen")
|
||||
|
||||
for user_id, email, username in users_without_username:
|
||||
if not username:
|
||||
# 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
|
||||
# Testen, ob die neuen Tabellen funktionieren
|
||||
test_new_tables()
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Fehler bei der Migration: {e}")
|
||||
conn.rollback()
|
||||
return False
|
||||
|
||||
finally:
|
||||
conn.close()
|
||||
logger.error(f"Fehler bei der Datenbank-Migration: {str(e)}")
|
||||
sys.exit(1)
|
||||
|
||||
def test_new_tables():
|
||||
"""Testet, ob die neuen Tabellen korrekt erstellt wurden."""
|
||||
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__":
|
||||
success = migrate_database()
|
||||
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.")
|
||||
main()
|
@ -1,10 +1,10 @@
|
||||
/**
|
||||
* Dark Mode Toggle Fix
|
||||
* Diese Datei stellt sicher, dass der Dark Mode Toggle Button korrekt funktioniert
|
||||
* Dark Mode Toggle Fix - Premium Edition
|
||||
* Diese Datei stellt sicher, dass der neue Premium Dark Mode Toggle Button korrekt funktioniert
|
||||
*/
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Dark Mode Toggle Button
|
||||
// Dark Mode Toggle Button (Premium Design)
|
||||
const darkModeToggle = document.getElementById('darkModeToggle');
|
||||
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) {
|
||||
// Finde die Icons im Button - versuche sowohl direkte Kinder als auch verschachtelte
|
||||
let sunIcon = darkModeToggle.querySelector('.sun-icon');
|
||||
let moonIcon = darkModeToggle.querySelector('.moon-icon');
|
||||
if (!darkModeToggle) return;
|
||||
|
||||
// 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) {
|
||||
console.warn('Icons nicht gefunden - erzeuge neue Icons');
|
||||
|
||||
// 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);
|
||||
console.warn('Premium Dark Mode Icons nicht gefunden');
|
||||
return;
|
||||
}
|
||||
|
||||
// Icons entsprechend dem Dark Mode Status anzeigen/verbergen
|
||||
// Animation für Übergänge
|
||||
if (isDark) {
|
||||
sunIcon.classList.add('hidden');
|
||||
moonIcon.classList.remove('hidden');
|
||||
// Dark Mode aktiviert - zeige Mond
|
||||
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 {
|
||||
sunIcon.classList.remove('hidden');
|
||||
moonIcon.classList.add('hidden');
|
||||
// Light Mode aktiviert - zeige Sonne
|
||||
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) {
|
||||
console.log(`Setze Dark Mode auf: ${enable ? 'Aktiviert' : 'Deaktiviert'}`);
|
||||
console.log(`🎨 Setze Premium Dark Mode auf: ${enable ? 'Aktiviert' : 'Deaktiviert'}`);
|
||||
|
||||
if (enable) {
|
||||
html.classList.add('dark');
|
||||
@ -82,7 +83,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
if (darkModeToggle) {
|
||||
darkModeToggle.setAttribute('aria-pressed', 'true');
|
||||
darkModeToggle.setAttribute('title', 'Light Mode aktivieren');
|
||||
// Button-Icons aktualisieren
|
||||
// Premium Button-Icons aktualisieren
|
||||
updateIcons(true);
|
||||
}
|
||||
} else {
|
||||
@ -93,7 +94,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
if (darkModeToggle) {
|
||||
darkModeToggle.setAttribute('aria-pressed', 'false');
|
||||
darkModeToggle.setAttribute('title', 'Dark Mode aktivieren');
|
||||
// Button-Icons aktualisieren
|
||||
// Premium Button-Icons aktualisieren
|
||||
updateIcons(false);
|
||||
}
|
||||
}
|
||||
@ -111,17 +112,33 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
window.dispatchEvent(new CustomEvent('darkModeChanged', {
|
||||
detail: { isDark: enable }
|
||||
}));
|
||||
|
||||
// Premium-Feedback
|
||||
console.log(`${enable ? '🌙' : '☀️'} Premium Design umgeschaltet auf: ${enable ? 'Dark Mode' : 'Light Mode'}`);
|
||||
}
|
||||
|
||||
// Toggle Dark Mode Funktion
|
||||
function toggleDarkMode() {
|
||||
const currentMode = isDarkMode();
|
||||
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) {
|
||||
// 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);
|
||||
darkModeToggle.parentNode.replaceChild(newDarkModeToggle, darkModeToggle);
|
||||
|
||||
@ -133,15 +150,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
|
||||
// Aktualisiere die Variable auf das neue Element
|
||||
darkModeToggle = newDarkModeToggle;
|
||||
const updatedToggle = document.getElementById('darkModeToggle');
|
||||
|
||||
// Initialen Status setzen
|
||||
const isDark = isDarkMode();
|
||||
setDarkMode(isDark);
|
||||
|
||||
console.log('Dark Mode Toggle Button erfolgreich initialisiert');
|
||||
console.log('✨ Premium Dark Mode Toggle Button erfolgreich initialisiert');
|
||||
} 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
|
||||
@ -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
|
||||
window.toggleDarkMode = toggleDarkMode;
|
||||
window.isDarkMode = isDarkMode;
|
||||
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');
|
||||
});
|
2
backend/app/static/js/fullcalendar/main.min.css
vendored
Normal file
2
backend/app/static/js/fullcalendar/main.min.css
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/* FullCalendar v6 CSS is embedded in the JavaScript bundle */
|
||||
/* This file is kept for template compatibility */
|
6
backend/app/static/js/fullcalendar/main.min.js
vendored
Normal file
6
backend/app/static/js/fullcalendar/main.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -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 %}
|
@ -50,6 +50,32 @@ LOG_EMOJIS = {
|
||||
'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
|
||||
def supports_color() -> bool:
|
||||
"""Prüft, ob das Terminal ANSI-Farben unterstützt."""
|
||||
@ -59,6 +85,14 @@ def supports_color() -> bool:
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
# Aktiviere VT100-Unterstützung unter Windows
|
||||
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
|
||||
except:
|
||||
return False
|
||||
|
Loading…
x
Reference in New Issue
Block a user