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

This commit is contained in:
Till Tomczak 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."""
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)

View File

@ -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()

View File

@ -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');
});

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': '📺'
}
# 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