📝 "🐛 Refactor backend files, improve documentation, and update UI components (#123)"

This commit is contained in:
Till Tomczak 2025-06-01 00:59:13 +02:00
parent 070f4a6165
commit 4dd3c4b1b1
13 changed files with 11286 additions and 142 deletions

View File

@ -5439,13 +5439,48 @@ def api_admin_database_status():
table_stats[table] = f"Error: {str(e)}"
# Connection-Pool-Status
pool_status = {
'pool_size': engine.pool.size(),
'checked_in': engine.pool.checkedin(),
'checked_out': engine.pool.checkedout(),
'overflow': engine.pool.overflow(),
'invalid': engine.pool.invalid()
}
pool_status = {}
try:
# StaticPool hat andere Methoden als andere Pool-Typen
if hasattr(engine.pool, 'size'):
pool_status['pool_size'] = engine.pool.size()
else:
pool_status['pool_size'] = 'N/A (StaticPool)'
if hasattr(engine.pool, 'checkedin'):
pool_status['checked_in'] = engine.pool.checkedin()
else:
pool_status['checked_in'] = 'N/A'
if hasattr(engine.pool, 'checkedout'):
pool_status['checked_out'] = engine.pool.checkedout()
else:
pool_status['checked_out'] = 'N/A'
if hasattr(engine.pool, 'overflow'):
pool_status['overflow'] = engine.pool.overflow()
else:
pool_status['overflow'] = 'N/A'
if hasattr(engine.pool, 'invalid'):
pool_status['invalid'] = engine.pool.invalid()
else:
pool_status['invalid'] = 'N/A'
# Zusätzliche StaticPool-spezifische Informationen
pool_status['pool_type'] = type(engine.pool).__name__
except Exception as pool_error:
app_logger.warning(f"Fehler beim Abrufen des Pool-Status: {str(pool_error)}")
pool_status = {
'pool_size': 'Error',
'checked_in': 'Error',
'checked_out': 'Error',
'overflow': 'Error',
'invalid': 'Error',
'pool_type': type(engine.pool).__name__,
'error': str(pool_error)
}
db_session.close()
@ -5567,10 +5602,20 @@ def api_admin_system_status():
try:
boot_time = psutil.boot_time()
uptime_seconds = int(time.time() - boot_time)
# Robuste uptime-Formatierung
try:
days = uptime_seconds // 86400
hours = (uptime_seconds % 86400) // 3600
minutes = ((uptime_seconds % 86400) % 3600) // 60
uptime_formatted = f"{days}d {hours}h {minutes}m"
except (ValueError, OverflowError, ZeroDivisionError):
uptime_formatted = f"{uptime_seconds}s"
uptime_info = {
'boot_time': datetime.fromtimestamp(boot_time).isoformat(),
'uptime_seconds': uptime_seconds,
'uptime_formatted': str(timedelta(seconds=uptime_seconds))
'uptime_formatted': uptime_formatted
}
except Exception:
uptime_info = {'error': 'Uptime-Informationen nicht verfügbar'}

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@

View File

@ -360,27 +360,237 @@
/* Glassmorphism Flash Messages */
.flash-message {
@apply fixed top-4 right-4 px-6 py-4 rounded-xl text-sm font-medium shadow-2xl transform transition-all duration-300 z-50 backdrop-blur-xl border border-white/20;
backdrop-filter: blur(20px) saturate(180%) brightness(120%);
-webkit-backdrop-filter: blur(20px) saturate(180%) brightness(120%);
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(255, 255, 255, 0.1);
animation: slide-down 0.3s ease-out;
@apply fixed top-4 right-4 px-6 py-4 rounded-2xl text-sm font-medium shadow-2xl transform transition-all duration-500 z-50 border;
/* Verstärkter Glassmorphism-Effekt */
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(40px) saturate(200%) brightness(130%) contrast(110%);
-webkit-backdrop-filter: blur(40px) saturate(200%) brightness(130%) contrast(110%);
border: 1px solid rgba(255, 255, 255, 0.25);
box-shadow:
0 32px 64px rgba(0, 0, 0, 0.25),
0 12px 24px rgba(0, 0, 0, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.4),
0 0 0 1px rgba(255, 255, 255, 0.1);
animation: flash-slide-in 0.5s cubic-bezier(0.4, 0, 0.2, 1);
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.dark .flash-message {
background: rgba(0, 0, 0, 0.2);
backdrop-filter: blur(40px) saturate(180%) brightness(120%) contrast(115%);
-webkit-backdrop-filter: blur(40px) saturate(180%) brightness(120%) contrast(115%);
border: 1px solid rgba(255, 255, 255, 0.15);
box-shadow:
0 32px 64px rgba(0, 0, 0, 0.6),
0 12px 24px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.2),
0 0 0 1px rgba(255, 255, 255, 0.05);
}
.flash-message:hover {
transform: translateY(-2px) scale(1.02);
box-shadow:
0 40px 80px rgba(0, 0, 0, 0.3),
0 16px 32px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.5),
0 0 0 1px rgba(255, 255, 255, 0.15);
}
.dark .flash-message:hover {
box-shadow:
0 40px 80px rgba(0, 0, 0, 0.7),
0 16px 32px rgba(0, 0, 0, 0.5),
inset 0 1px 0 rgba(255, 255, 255, 0.3),
0 0 0 1px rgba(255, 255, 255, 0.1);
}
.flash-message.info {
@apply bg-blue-500/70 dark:bg-blue-600/70 text-white;
@apply text-blue-100;
background: linear-gradient(135deg,
rgba(59, 130, 246, 0.2) 0%,
rgba(147, 197, 253, 0.15) 50%,
rgba(59, 130, 246, 0.1) 100%);
border: 1px solid rgba(59, 130, 246, 0.3);
}
.flash-message.success {
@apply bg-green-500/70 dark:bg-green-600/70 text-white;
@apply text-green-100;
background: linear-gradient(135deg,
rgba(34, 197, 94, 0.2) 0%,
rgba(134, 239, 172, 0.15) 50%,
rgba(34, 197, 94, 0.1) 100%);
border: 1px solid rgba(34, 197, 94, 0.3);
}
.flash-message.warning {
@apply bg-yellow-500/70 dark:bg-yellow-600/70 text-white;
@apply text-yellow-100;
background: linear-gradient(135deg,
rgba(245, 158, 11, 0.2) 0%,
rgba(252, 211, 77, 0.15) 50%,
rgba(245, 158, 11, 0.1) 100%);
border: 1px solid rgba(245, 158, 11, 0.3);
}
.flash-message.error {
@apply bg-red-500/70 dark:bg-red-600/70 text-white;
@apply text-red-100;
background: linear-gradient(135deg,
rgba(239, 68, 68, 0.2) 0%,
rgba(252, 165, 165, 0.15) 50%,
rgba(239, 68, 68, 0.1) 100%);
border: 1px solid rgba(239, 68, 68, 0.3);
}
/* Flash Message Animation */
@keyframes flash-slide-in {
0% {
opacity: 0;
transform: translateX(100%) translateY(-20px) scale(0.9);
backdrop-filter: blur(0px);
}
50% {
opacity: 0.8;
transform: translateX(20px) translateY(-10px) scale(1.05);
backdrop-filter: blur(20px);
}
100% {
opacity: 1;
transform: translateX(0) translateY(0) scale(1);
backdrop-filter: blur(40px);
}
}
@keyframes flash-slide-out {
0% {
opacity: 1;
transform: translateX(0) translateY(0) scale(1);
}
100% {
opacity: 0;
transform: translateX(100%) translateY(-20px) scale(0.9);
}
}
.flash-message.hiding {
animation: flash-slide-out 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
/* Do Not Disturb System Styles */
.dnd-toggle {
@apply relative inline-flex items-center h-6 rounded-full w-11 transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500;
background: rgba(156, 163, 175, 0.3);
backdrop-filter: blur(10px);
border: 1px solid rgba(156, 163, 175, 0.2);
}
.dnd-toggle.active {
background: rgba(239, 68, 68, 0.3);
border: 1px solid rgba(239, 68, 68, 0.4);
}
.dnd-toggle-slider {
@apply inline-block h-4 w-4 rounded-full shadow-lg transform transition-transform duration-300;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow:
0 4px 8px rgba(0, 0, 0, 0.2),
0 2px 4px rgba(0, 0, 0, 0.1);
margin: 0.125rem;
}
.dnd-toggle.active .dnd-toggle-slider {
transform: translateX(1.25rem);
background: rgba(255, 255, 255, 1);
box-shadow:
0 6px 12px rgba(239, 68, 68, 0.3),
0 3px 6px rgba(239, 68, 68, 0.2);
}
.dnd-indicator {
@apply fixed top-4 left-4 z-50 flex items-center px-3 py-2 rounded-lg text-sm font-medium transition-all duration-300;
background: rgba(239, 68, 68, 0.1);
backdrop-filter: blur(20px) saturate(150%);
-webkit-backdrop-filter: blur(20px) saturate(150%);
border: 1px solid rgba(239, 68, 68, 0.3);
color: rgb(239, 68, 68);
transform: translateY(-100%);
opacity: 0;
}
.dnd-indicator.active {
transform: translateY(0);
opacity: 1;
}
.dnd-modal {
@apply fixed inset-0 z-50 flex items-center justify-center p-4;
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
}
.dnd-modal-content {
@apply w-full max-w-md rounded-2xl p-6 shadow-2xl transform transition-all;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(40px) saturate(200%) brightness(120%);
-webkit-backdrop-filter: blur(40px) saturate(200%) brightness(120%);
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow:
0 25px 50px rgba(0, 0, 0, 0.25),
0 8px 16px rgba(0, 0, 0, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.4);
}
.dark .dnd-modal-content {
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(40px) saturate(180%) brightness(110%);
-webkit-backdrop-filter: blur(40px) saturate(180%) brightness(110%);
border: 1px solid rgba(255, 255, 255, 0.15);
box-shadow:
0 25px 50px rgba(0, 0, 0, 0.6),
0 8px 16px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
/* DND Flash Message Override */
.flash-message.dnd-suppressed {
animation: flash-fade-in 0.3s ease-out;
opacity: 0.3;
transform: scale(0.95);
pointer-events: none;
}
@keyframes flash-fade-in {
0% {
opacity: 0;
transform: scale(0.9);
}
100% {
opacity: 0.3;
transform: scale(0.95);
}
}
/* Notification Counter */
.dnd-counter {
@apply absolute -top-2 -right-2 bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center font-bold;
background: rgba(239, 68, 68, 0.9);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
animation: dnd-counter-bounce 0.5s ease-out;
}
@keyframes dnd-counter-bounce {
0% {
transform: scale(0);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}
@keyframes slide-down {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -951,136 +951,103 @@
}
/**
* Toast-Nachricht anzeigen
* Erweiterte Flash-Nachricht anzeigen mit glasigen Effekten
* @param {string} message - Nachrichtentext
* @param {string} type - Nachrichtentyp (success, error, info, warning)
* @param {number} duration - Anzeigedauer in Millisekunden (Standard: 5000)
*/
function showToast(message, type = 'info') {
// Prüfen, ob Toast-Container existiert
let toastContainer = document.getElementById('toast-container');
function showFlashMessage(message, type = 'info', duration = 5000) {
// Unique ID für die Nachricht
const messageId = 'flash-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
// Falls nicht, erstellen wir einen
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.id = 'toast-container';
toastContainer.className = 'fixed top-4 right-4 z-50 flex flex-col space-y-2';
document.body.appendChild(toastContainer);
// Flash-Message-Element erstellen
const flashElement = document.createElement('div');
flashElement.id = messageId;
flashElement.className = `flash-message ${type}`;
// Icon basierend auf Typ
let icon = '';
switch(type) {
case 'success':
icon = `<svg class="w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>`;
break;
case 'error':
icon = `<svg class="w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
</svg>`;
break;
case 'warning':
icon = `<svg class="w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
</svg>`;
break;
case 'info':
default:
icon = `<svg class="w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
</svg>`;
}
// Toast-Element erstellen
const toast = document.createElement('div');
toast.className = `flex items-center p-4 mb-4 text-sm rounded-lg shadow-lg transition-all transform translate-x-0 opacity-100 ${getToastTypeClass(type)}`;
toast.innerHTML = `
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 ${getToastIconClass(type)}">
${getToastIcon(type)}
// Inhalt der Flash Message
flashElement.innerHTML = `
<div class="flex items-center">
${icon}
<div class="flex-1">
<p class="font-medium text-sm">${message}</p>
</div>
<button class="flash-close-btn ml-4 text-current opacity-70 hover:opacity-100 transition-opacity duration-200"
onclick="closeFlashMessage('${messageId}')">
<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="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div class="ml-3 text-sm font-normal">${message}</div>
<button type="button" class="ml-auto -mx-1.5 -my-1.5 rounded-lg p-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 focus:ring-2 focus:ring-gray-300 dark:focus:ring-gray-600 inline-flex h-8 w-8" aria-label="Schließen">
<span class="sr-only">Schließen</span>
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
</button>
`;
// Toast zum Container hinzufügen
toastContainer.appendChild(toast);
// Flash Message zum DOM hinzufügen
document.body.appendChild(flashElement);
// Schließen-Button-Event
const closeButton = toast.querySelector('button');
closeButton.addEventListener('click', () => {
dismissToast(toast);
// Flash Messages vertikal stapeln
repositionFlashMessages();
// Nach der angegebenen Zeit automatisch entfernen
setTimeout(() => {
closeFlashMessage(messageId);
}, duration);
return messageId;
}
/**
* Flash Message schließen
* @param {string} messageId - ID der zu schließenden Nachricht
*/
function closeFlashMessage(messageId) {
const flashElement = document.getElementById(messageId);
if (flashElement) {
flashElement.classList.add('hiding');
setTimeout(() => {
if (flashElement.parentNode) {
flashElement.parentNode.removeChild(flashElement);
}
repositionFlashMessages();
}, 400); // Dauer der Ausblende-Animation
}
}
/**
* Flash Messages neu positionieren für Stapel-Effekt
*/
function repositionFlashMessages() {
const flashMessages = document.querySelectorAll('.flash-message:not(.hiding)');
flashMessages.forEach((flash, index) => {
flash.style.top = `${16 + (index * 80)}px`; // 16px base + 80px pro Message
flash.style.right = '16px';
flash.style.zIndex = 50 - index; // Neueste Messages haben höheren z-index
});
// Toast nach 5 Sekunden automatisch ausblenden
setTimeout(() => {
dismissToast(toast);
}, 5000);
}
/**
* Toast ausblenden und nach Animation entfernen
* @param {HTMLElement} toast - Toast-Element
*/
function dismissToast(toast) {
toast.classList.replace('translate-x-0', 'translate-x-full');
toast.classList.replace('opacity-100', 'opacity-0');
setTimeout(() => {
toast.remove();
}, 300);
}
/**
* CSS-Klassen für Toast-Typ
* @param {string} type - Nachrichtentyp
* @returns {string} CSS-Klassen
*/
function getToastTypeClass(type) {
switch (type) {
case 'success':
return 'text-green-800 bg-green-50 dark:bg-green-900/30 dark:text-green-300';
case 'error':
return 'text-red-800 bg-red-50 dark:bg-red-900/30 dark:text-red-300';
case 'warning':
return 'text-yellow-800 bg-yellow-50 dark:bg-yellow-900/30 dark:text-yellow-300';
case 'info':
default:
return 'text-blue-800 bg-blue-50 dark:bg-blue-900/30 dark:text-blue-300';
}
}
/**
* CSS-Klassen für Toast-Icon
* @param {string} type - Nachrichtentyp
* @returns {string} CSS-Klassen
*/
function getToastIconClass(type) {
switch (type) {
case 'success':
return 'bg-green-100 text-green-500 dark:bg-green-800 dark:text-green-200 rounded-lg';
case 'error':
return 'bg-red-100 text-red-500 dark:bg-red-800 dark:text-red-200 rounded-lg';
case 'warning':
return 'bg-yellow-100 text-yellow-500 dark:bg-yellow-800 dark:text-yellow-200 rounded-lg';
case 'info':
default:
return 'bg-blue-100 text-blue-500 dark:bg-blue-800 dark:text-blue-200 rounded-lg';
}
}
/**
* SVG-Icon für Toast-Typ
* @param {string} type - Nachrichtentyp
* @returns {string} SVG-Markup
*/
function getToastIcon(type) {
switch (type) {
case 'success':
return '<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path></svg>';
case 'error':
return '<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>';
case 'warning':
return '<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>';
case 'info':
default:
return '<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path></svg>';
}
}
/**
* Flash-Nachricht anzeigen
* @param {string} message - Nachrichtentext
* @param {string} type - Nachrichtentyp (success, error, info, warning)
*/
function showFlashMessage(message, type = 'info') {
// Toast-Funktion verwenden, wenn verfügbar
if (typeof showToast === 'function') {
showToast(message, type);
} else {
// Fallback-Lösung, wenn Toast nicht verfügbar ist
alert(message);
}
}
/**
@ -1224,6 +1191,581 @@
}
}
/**
* Do Not Disturb Manager
* Verwaltet den Do Not Disturb-Modus für Flash Messages und Benachrichtigungen
*/
class DoNotDisturbManager {
constructor() {
this.isActive = false;
this.suppressedMessages = [];
this.settings = {
allowCritical: true,
allowErrorsOnly: false,
suppressDuration: 60, // Minuten
autoDisable: true
};
this.suppressEndTime = null;
this.indicator = null;
this.counter = 0;
this.init();
}
/**
* Do Not Disturb-System initialisieren
*/
init() {
this.loadSettings();
this.createIndicator();
this.setupEventListeners();
this.checkAutoDisable();
console.log('🔕 Do Not Disturb Manager erfolgreich initialisiert');
}
/**
* Einstellungen aus localStorage laden
*/
loadSettings() {
try {
const saved = localStorage.getItem('dnd-settings');
if (saved) {
this.settings = { ...this.settings, ...JSON.parse(saved) };
}
const state = localStorage.getItem('dnd-state');
if (state) {
const savedState = JSON.parse(state);
this.isActive = savedState.isActive || false;
this.suppressEndTime = savedState.suppressEndTime ? new Date(savedState.suppressEndTime) : null;
this.suppressedMessages = savedState.suppressedMessages || [];
this.counter = savedState.counter || 0;
}
} catch (error) {
console.error('Fehler beim Laden der DND-Einstellungen:', error);
}
}
/**
* Einstellungen in localStorage speichern
*/
saveSettings() {
try {
localStorage.setItem('dnd-settings', JSON.stringify(this.settings));
localStorage.setItem('dnd-state', JSON.stringify({
isActive: this.isActive,
suppressEndTime: this.suppressEndTime,
suppressedMessages: this.suppressedMessages,
counter: this.counter
}));
} catch (error) {
console.error('Fehler beim Speichern der DND-Einstellungen:', error);
}
}
/**
* DND-Indikator erstellen
*/
createIndicator() {
this.indicator = document.createElement('div');
this.indicator.className = 'dnd-indicator';
this.indicator.innerHTML = `
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zM9 8a1 1 0 012 0v4a1 1 0 11-2 0V8z" clip-rule="evenodd"/>
</svg>
<span class="dnd-text">Nicht stören</span>
<span class="dnd-counter-badge ml-2 px-2 py-1 text-xs rounded-full bg-red-500 text-white hidden">0</span>
`;
this.indicator.addEventListener('click', () => this.showSettings());
document.body.appendChild(this.indicator);
this.updateIndicator();
}
/**
* Event-Listener einrichten
*/
setupEventListeners() {
// Original showFlashMessage überschreiben
const originalShowFlashMessage = window.showFlashMessage;
window.showFlashMessage = (message, type = 'info') => {
this.handleFlashMessage(message, type, originalShowFlashMessage);
};
// Original showToast überschreiben
if (window.showToast) {
const originalShowToast = window.showToast;
window.showToast = (message, type = 'info', duration) => {
this.handleToastMessage(message, type, duration, originalShowToast);
};
}
// Periodisch Auto-Disable prüfen
setInterval(() => this.checkAutoDisable(), 60000); // Jede Minute
}
/**
* Flash Message verarbeiten
*/
handleFlashMessage(message, type, originalFunction) {
if (this.shouldSuppressMessage(type)) {
this.addSuppressedMessage(message, type, 'flash');
this.showSuppressedMessage(message, type);
} else {
originalFunction(message, type);
}
}
/**
* Toast Message verarbeiten
*/
handleToastMessage(message, type, duration, originalFunction) {
if (this.shouldSuppressMessage(type)) {
this.addSuppressedMessage(message, type, 'toast');
this.showSuppressedMessage(message, type);
} else {
originalFunction(message, type, duration);
}
}
/**
* Prüfen, ob Nachricht unterdrückt werden soll
*/
shouldSuppressMessage(type) {
if (!this.isActive) return false;
// Kritische Nachrichten immer anzeigen (falls eingestellt)
if (this.settings.allowCritical && (type === 'error' || type === 'critical')) {
return false;
}
// Nur Fehler anzeigen (falls eingestellt)
if (this.settings.allowErrorsOnly && type !== 'error') {
return true;
}
return true;
}
/**
* Unterdrückte Nachricht hinzufügen
*/
addSuppressedMessage(message, type, source) {
const suppressedMessage = {
id: Date.now(),
message,
type,
source,
timestamp: new Date(),
read: false
};
this.suppressedMessages.unshift(suppressedMessage);
this.counter++;
// Nur die letzten 50 Nachrichten behalten
if (this.suppressedMessages.length > 50) {
this.suppressedMessages = this.suppressedMessages.slice(0, 50);
}
this.updateIndicator();
this.saveSettings();
}
/**
* Gedämpfte Version der Nachricht anzeigen
*/
showSuppressedMessage(message, type) {
const suppressedFlash = document.createElement('div');
suppressedFlash.className = `flash-message dnd-suppressed ${type}`;
suppressedFlash.innerHTML = `
<div class="flex items-center">
<svg class="w-4 h-4 mr-2 opacity-50" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zM9 8a1 1 0 012 0v4a1 1 0 11-2 0V8z" clip-rule="evenodd"/>
</svg>
<span class="opacity-70 text-xs">Nachricht unterdrückt</span>
</div>
`;
suppressedFlash.style.top = '4rem';
document.body.appendChild(suppressedFlash);
setTimeout(() => {
suppressedFlash.remove();
}, 2000);
}
/**
* DND-Modus aktivieren
*/
enable(duration = null) {
this.isActive = true;
if (duration) {
this.suppressEndTime = new Date(Date.now() + duration * 60000);
} else {
this.suppressEndTime = null;
}
this.updateIndicator();
this.saveSettings();
console.log('🔕 Do Not Disturb aktiviert', duration ? `für ${duration} Minuten` : 'dauerhaft');
}
/**
* DND-Modus deaktivieren
*/
disable() {
this.isActive = false;
this.suppressEndTime = null;
this.updateIndicator();
this.saveSettings();
console.log('🔔 Do Not Disturb deaktiviert');
}
/**
* DND-Modus umschalten
*/
toggle() {
if (this.isActive) {
this.disable();
} else {
this.showSettings();
}
}
/**
* Auto-Disable prüfen
*/
checkAutoDisable() {
if (this.isActive && this.suppressEndTime && new Date() >= this.suppressEndTime) {
this.disable();
if (window.showToast) {
window.showToast('Do Not Disturb automatisch deaktiviert', 'info');
}
}
}
/**
* Indikator aktualisieren
*/
updateIndicator() {
if (!this.indicator) return;
if (this.isActive) {
this.indicator.classList.add('active');
// Counter Badge aktualisieren
const badge = this.indicator.querySelector('.dnd-counter-badge');
if (this.counter > 0) {
badge.textContent = this.counter > 99 ? '99+' : this.counter;
badge.classList.remove('hidden');
} else {
badge.classList.add('hidden');
}
// Zeitanzeige
const text = this.indicator.querySelector('.dnd-text');
if (this.suppressEndTime) {
const remaining = Math.ceil((this.suppressEndTime - new Date()) / 60000);
text.textContent = `Nicht stören (${remaining}min)`;
} else {
text.textContent = 'Nicht stören';
}
} else {
this.indicator.classList.remove('active');
}
}
/**
* Einstellungs-Modal anzeigen
*/
showSettings() {
const modal = document.createElement('div');
modal.className = 'dnd-modal';
modal.innerHTML = `
<div class="dnd-modal-content">
<div class="flex items-center justify-between mb-6">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">
🔕 Nicht stören
</h3>
<button class="dnd-close-btn text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200">
<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"/>
</svg>
</button>
</div>
<div class="space-y-4">
<!-- Schnell-Aktionen -->
<div class="grid grid-cols-2 gap-3">
<button class="dnd-quick-btn btn-primary" data-duration="30">
30 Min
</button>
<button class="dnd-quick-btn btn-primary" data-duration="60">
1 Stunde
</button>
<button class="dnd-quick-btn btn-primary" data-duration="480">
8 Stunden
</button>
<button class="dnd-quick-btn btn-primary" data-duration="0">
Dauerhaft
</button>
</div>
<!-- Erweiterte Einstellungen -->
<div class="pt-4 border-t border-gray-200/30 dark:border-slate-700/30 space-y-3">
<label class="flex items-center space-x-3">
<input type="checkbox" class="dnd-setting" data-setting="allowCritical"
${this.settings.allowCritical ? 'checked' : ''}>
<span class="text-sm text-slate-700 dark:text-slate-300">
Kritische Fehler anzeigen
</span>
</label>
<label class="flex items-center space-x-3">
<input type="checkbox" class="dnd-setting" data-setting="allowErrorsOnly"
${this.settings.allowErrorsOnly ? 'checked' : ''}>
<span class="text-sm text-slate-700 dark:text-slate-300">
Nur Fehler anzeigen
</span>
</label>
</div>
<!-- Unterdrückte Nachrichten -->
${this.suppressedMessages.length > 0 ? `
<div class="pt-4 border-t border-gray-200/30 dark:border-slate-700/30">
<div class="flex items-center justify-between mb-3">
<h4 class="text-sm font-medium text-slate-900 dark:text-white">
Unterdrückte Nachrichten (${this.suppressedMessages.length})
</h4>
<button class="dnd-clear-btn text-xs text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200">
Alle löschen
</button>
</div>
<div class="max-h-32 overflow-y-auto space-y-2">
${this.suppressedMessages.slice(0, 5).map(msg => `
<div class="text-xs p-2 rounded bg-slate-100/50 dark:bg-slate-800/50">
<span class="font-medium text-${msg.type === 'error' ? 'red' : msg.type === 'warning' ? 'yellow' : msg.type === 'success' ? 'green' : 'blue'}-600 dark:text-${msg.type === 'error' ? 'red' : msg.type === 'warning' ? 'yellow' : msg.type === 'success' ? 'green' : 'blue'}-400">
${msg.type.toUpperCase()}:
</span>
<span class="text-slate-600 dark:text-slate-400">
${msg.message}
</span>
<div class="text-xs text-slate-400 dark:text-slate-500 mt-1">
${msg.timestamp.toLocaleTimeString()}
</div>
</div>
`).join('')}
${this.suppressedMessages.length > 5 ? `
<div class="text-xs text-center text-slate-500 dark:text-slate-400 py-2">
... und ${this.suppressedMessages.length - 5} weitere
</div>
` : ''}
</div>
</div>
` : ''}
<!-- Aktions-Buttons -->
<div class="flex space-x-3 pt-4">
${this.isActive ? `
<button class="dnd-disable-btn btn-secondary flex-1">
Deaktivieren
</button>
` : ''}
<button class="dnd-close-btn btn-primary flex-1">
Schließen
</button>
</div>
</div>
</div>
`;
// Event Listeners
modal.addEventListener('click', (e) => {
if (e.target === modal || e.target.classList.contains('dnd-close-btn')) {
modal.remove();
}
if (e.target.classList.contains('dnd-quick-btn')) {
const duration = parseInt(e.target.dataset.duration);
if (duration === 0) {
this.enable();
} else {
this.enable(duration);
}
modal.remove();
}
if (e.target.classList.contains('dnd-disable-btn')) {
this.disable();
modal.remove();
}
if (e.target.classList.contains('dnd-clear-btn')) {
this.clearSuppressedMessages();
modal.remove();
this.showSettings(); // Modal neu laden
}
if (e.target.classList.contains('dnd-setting')) {
const setting = e.target.dataset.setting;
this.settings[setting] = e.target.checked;
this.saveSettings();
}
});
document.body.appendChild(modal);
}
/**
* Unterdrückte Nachrichten löschen
*/
clearSuppressedMessages() {
this.suppressedMessages = [];
this.counter = 0;
this.updateIndicator();
this.saveSettings();
}
/**
* Unterdrückte Nachrichten abrufen
*/
getSuppressedMessages() {
return [...this.suppressedMessages];
}
/**
* Status abrufen
*/
getStatus() {
return {
isActive: this.isActive,
suppressEndTime: this.suppressEndTime,
suppressedCount: this.suppressedMessages.length,
settings: { ...this.settings }
};
}
}
/**
* Navbar Do Not Disturb Integration
* Verbindet den DND-Button in der Navbar mit dem DoNotDisturbManager
*/
class NavbarDNDIntegration {
constructor(dndManager) {
this.dndManager = dndManager;
this.button = document.getElementById('dndToggle');
this.counter = document.getElementById('dndCounter');
this.iconOff = null;
this.iconOn = null;
this.tooltipOff = null;
this.tooltipOn = null;
this.init();
}
/**
* Navbar DND Integration initialisieren
*/
init() {
if (!this.button) {
console.log(' DND Button nicht gefunden - Navbar Integration deaktiviert');
return;
}
this.iconOff = this.button.querySelector('.dnd-icon-off');
this.iconOn = this.button.querySelector('.dnd-icon-on');
this.tooltipOff = this.button.querySelector('.dnd-tooltip-off');
this.tooltipOn = this.button.querySelector('.dnd-tooltip-on');
// Event Listener
this.button.addEventListener('click', () => this.handleButtonClick());
// Initial state setzen
this.updateButton();
// Status-Änderungen überwachen
setInterval(() => this.updateButton(), 1000);
console.log('🔕 Navbar DND Integration erfolgreich initialisiert');
}
/**
* Button-Click Handler
*/
handleButtonClick() {
this.dndManager.toggle();
this.updateButton();
}
/**
* Button-Erscheinungsbild aktualisieren
*/
updateButton() {
if (!this.button) return;
const status = this.dndManager.getStatus();
if (status.isActive) {
// DND ist aktiv
this.button.classList.add('dnd-active');
if (this.iconOff) {
this.iconOff.style.opacity = '0';
this.iconOff.style.transform = 'scale(0.75)';
}
if (this.iconOn) {
this.iconOn.style.opacity = '1';
this.iconOn.style.transform = 'scale(1)';
}
if (this.tooltipOff) this.tooltipOff.classList.add('hidden');
if (this.tooltipOn) this.tooltipOn.classList.remove('hidden');
// Counter aktualisieren
if (this.counter && status.suppressedCount > 0) {
this.counter.textContent = status.suppressedCount > 99 ? '99+' : status.suppressedCount;
this.counter.classList.remove('hidden');
} else if (this.counter) {
this.counter.classList.add('hidden');
}
// Button-Erscheinungsbild
this.button.style.background = 'rgba(239, 68, 68, 0.1)';
this.button.style.borderColor = 'rgba(239, 68, 68, 0.3)';
} else {
// DND ist inaktiv
this.button.classList.remove('dnd-active');
if (this.iconOff) {
this.iconOff.style.opacity = '1';
this.iconOff.style.transform = 'scale(1)';
}
if (this.iconOn) {
this.iconOn.style.opacity = '0';
this.iconOn.style.transform = 'scale(0.75)';
}
if (this.tooltipOff) this.tooltipOff.classList.remove('hidden');
if (this.tooltipOn) this.tooltipOn.classList.add('hidden');
if (this.counter) this.counter.classList.add('hidden');
// Button-Erscheinungsbild zurücksetzen
this.button.style.background = '';
this.button.style.borderColor = '';
}
}
}
// Initialisierung aller UI-Komponenten
document.addEventListener('DOMContentLoaded', function() {
// Toast-Manager
@ -1253,11 +1795,20 @@
// Navbar Scroll Manager für Glassmorphism-Effekte
window.MYP.UI.navbarScroll = new NavbarScrollManager();
// Do Not Disturb Manager
window.MYP.UI.doNotDisturb = new DoNotDisturbManager();
// Navbar DND Integration
window.MYP.UI.navbarDND = new NavbarDNDIntegration(window.MYP.UI.doNotDisturb);
// Convenience-Methoden
window.showToast = (message, type, duration) => window.MYP.UI.toast.show(message, type, duration);
window.showModal = (modalId, options) => window.MYP.UI.modal.open(modalId, options);
window.hideModal = (modalId) => window.MYP.UI.modal.close(modalId);
window.toggleDarkMode = () => window.MYP.UI.darkMode.setDarkMode(!window.MYP.UI.darkMode.isDarkMode());
window.toggleDoNotDisturb = () => window.MYP.UI.doNotDisturb.toggle();
window.enableDoNotDisturb = (duration) => window.MYP.UI.doNotDisturb.enable(duration);
window.disableDoNotDisturb = () => window.MYP.UI.doNotDisturb.disable();
// Event-Listener für data-Attribute
document.addEventListener('click', (e) => {
@ -1278,17 +1829,37 @@
const dropdownId = e.target.closest('[data-dropdown-toggle]').getAttribute('data-dropdown-toggle');
window.MYP.UI.dropdown.toggle(dropdownId);
}
// Do Not Disturb Toggle
if (e.target.matches('[data-dnd-toggle]') || e.target.closest('[data-dnd-toggle]')) {
window.MYP.UI.doNotDisturb.toggle();
}
});
console.log('✅ MYP UI Components erfolgreich initialisiert - Benutzeroberfläche bereit');
// Keyboard Shortcuts
document.addEventListener('keydown', (e) => {
// Ctrl/Cmd + Shift + D für Do Not Disturb Toggle
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 'd') {
e.preventDefault();
window.MYP.UI.doNotDisturb.toggle();
}
// Escape für alle Modals schließen
if (e.key === 'Escape') {
window.MYP.UI.modal.closeTopModal();
}
});
console.log('✅ MYP UI Components erfolgreich initialisiert - Erweiterte Benutzeroberfläche mit Glassmorphism und Do Not Disturb bereit');
});
// Globale Variablen für erweiterte Flash Messages
window.showFlashMessage = showFlashMessage;
window.closeFlashMessage = closeFlashMessage;
// Globale Variable für Toast-Funktion
window.showToast = showToast;
// Globale Variable für API-Aufrufe
window.apiCall = apiCall;
// Globale Variable für Flash-Nachrichten
window.showFlashMessage = showFlashMessage;
})();

View File

@ -0,0 +1,347 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Drucker bearbeiten - MYP Admin</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body class="bg-gray-100">
<div class="min-h-screen py-8">
<div class="max-w-2xl mx-auto">
<!-- Header -->
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<i class="fas fa-edit text-blue-600 text-2xl"></i>
<h1 class="text-2xl font-bold text-gray-800">Drucker bearbeiten</h1>
</div>
<a href="{{ url_for('admin_page', tab='printers') }}"
class="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-lg transition-colors">
<i class="fas fa-arrow-left mr-2"></i>Zurück
</a>
</div>
<div class="mt-4 p-3 bg-blue-50 rounded-lg">
<p class="text-sm text-blue-700">
<i class="fas fa-info-circle mr-2"></i>
<strong>Drucker-ID:</strong> {{ printer.id }} |
<strong>Erstellt am:</strong> {{ printer.created_at[:10] if printer.created_at else 'Unbekannt' }}
</p>
</div>
</div>
<!-- Formular -->
<div class="bg-white rounded-lg shadow-md p-6">
<form action="{{ url_for('admin_update_printer_form', printer_id=printer.id) }}" method="POST" class="space-y-6">
<!-- CSRF Token -->
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<!-- Name -->
<div>
<label for="name" class="block text-sm font-medium text-gray-700 mb-2">
<i class="fas fa-tag mr-2"></i>Drucker-Name *
</label>
<input type="text"
id="name"
name="name"
required
value="{{ printer.name }}"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="3D-Drucker Raum A001">
</div>
<!-- IP-Adresse -->
<div>
<label for="ip_address" class="block text-sm font-medium text-gray-700 mb-2">
<i class="fas fa-network-wired mr-2"></i>IP-Adresse *
</label>
<input type="text"
id="ip_address"
name="ip_address"
required
value="{{ printer.plug_ip }}"
pattern="^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="192.168.1.100">
<p class="text-sm text-gray-500 mt-1">IP-Adresse der Tapo-Steckdose</p>
</div>
<!-- Modell -->
<div>
<label for="model" class="block text-sm font-medium text-gray-700 mb-2">
<i class="fas fa-cogs mr-2"></i>Drucker-Modell
</label>
<input type="text"
id="model"
name="model"
value="{{ printer.model }}"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Ender 3 V2">
</div>
<!-- Standort -->
<div>
<label for="location" class="block text-sm font-medium text-gray-700 mb-2">
<i class="fas fa-map-marker-alt mr-2"></i>Standort
</label>
<input type="text"
id="location"
name="location"
value="{{ printer.location }}"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Raum A001, Erdgeschoss">
</div>
<!-- Beschreibung -->
<div>
<label for="description" class="block text-sm font-medium text-gray-700 mb-2">
<i class="fas fa-comment mr-2"></i>Beschreibung
</label>
<textarea id="description"
name="description"
rows="3"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Zusätzliche Informationen zum Drucker...">{{ printer.description or '' }}</textarea>
</div>
<!-- Status -->
<div>
<label for="status" class="block text-sm font-medium text-gray-700 mb-2">
<i class="fas fa-circle mr-2"></i>Aktueller Status
</label>
<select id="status"
name="status"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent">
<option value="available" {{ 'selected' if printer.status == 'available' else '' }}>Verfügbar</option>
<option value="offline" {{ 'selected' if printer.status == 'offline' else '' }}>Offline</option>
<option value="maintenance" {{ 'selected' if printer.status == 'maintenance' else '' }}>Wartung</option>
<option value="online" {{ 'selected' if printer.status == 'online' else '' }}>Online</option>
<option value="printing" {{ 'selected' if printer.status == 'printing' else '' }}>Druckt</option>
</select>
</div>
<!-- Aktiv-Status -->
<div>
<label class="flex items-center space-x-3">
<input type="checkbox"
name="is_active"
{{ 'checked' if printer.active else '' }}
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 focus:ring-2">
<span class="text-sm font-medium text-gray-700">
<i class="fas fa-power-off mr-2"></i>Drucker aktiv
</span>
</label>
<p class="text-sm text-gray-500 mt-1">Inaktive Drucker werden nicht für neue Aufträge verwendet</p>
</div>
<!-- Erweiterte Informationen -->
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
<h3 class="text-sm font-semibold text-gray-700 mb-3">
<i class="fas fa-info-circle mr-2"></i>Drucker-Informationen
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
<div>
<span class="font-medium text-gray-600">MAC-Adresse:</span>
<span class="text-gray-800">{{ printer.mac_address or 'Nicht verfügbar' }}</span>
</div>
<div>
<span class="font-medium text-gray-600">Letzter Check:</span>
<span class="text-gray-800">{{ printer.last_checked or 'Nie' }}</span>
</div>
</div>
</div>
<!-- Warnung -->
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div class="flex">
<i class="fas fa-exclamation-triangle text-yellow-500 mt-0.5 mr-3"></i>
<div class="text-sm text-yellow-700">
<p class="font-semibold mb-1">Wichtige Hinweise:</p>
<ul class="list-disc list-inside space-y-1">
<li>Änderungen an der IP-Adresse können die Verbindung unterbrechen</li>
<li>Stellen Sie sicher, dass die Tapo-Steckdose unter der neuen IP erreichbar ist</li>
<li>Bei Status-Änderungen werden laufende Jobs möglicherweise beeinflusst</li>
</ul>
</div>
</div>
</div>
<!-- Aktionen -->
<div class="flex space-x-3 pt-4">
<button type="submit"
class="flex-1 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition-colors">
<i class="fas fa-save mr-2"></i>Änderungen speichern
</button>
<a href="{{ url_for('admin_page', tab='printers') }}"
class="flex-1 bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-lg text-center transition-colors">
<i class="fas fa-times mr-2"></i>Abbrechen
</a>
</div>
</form>
</div>
<!-- Zusätzliche Aktionen -->
<div class="bg-white rounded-lg shadow-md p-6 mt-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">
<i class="fas fa-tools mr-2"></i>Drucker-Aktionen
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<button data-printer-id="{{ printer.id }}"
data-action="test"
class="printer-action-btn bg-green-600 hover:bg-green-700 text-white px-4 py-3 rounded-lg transition-colors">
<i class="fas fa-plug mr-2"></i>Verbindung testen
</button>
<button data-printer-id="{{ printer.id }}"
data-action="toggle"
class="printer-action-btn bg-orange-600 hover:bg-orange-700 text-white px-4 py-3 rounded-lg transition-colors">
<i class="fas fa-power-off mr-2"></i>Ein/Ausschalten
</button>
</div>
</div>
</div>
</div>
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="fixed top-4 right-4 z-50 space-y-2">
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else category }} bg-{{ 'red' if category == 'error' else 'green' }}-100 border border-{{ 'red' if category == 'error' else 'green' }}-400 text-{{ 'red' if category == 'error' else 'green' }}-700 px-4 py-3 rounded-lg shadow-md">
<div class="flex items-center">
<i class="fas fa-{{ 'exclamation-triangle' if category == 'error' else 'check-circle' }} mr-2"></i>
{{ message }}
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<!-- JavaScript -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.querySelector('form');
const nameInput = document.getElementById('name');
const ipInput = document.getElementById('ip_address');
// IP-Adresse-Validierung
ipInput.addEventListener('blur', function() {
const ip = this.value;
const ipRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
if (ip && !ipRegex.test(ip)) {
this.classList.add('border-red-500');
this.classList.remove('border-gray-300');
} else {
this.classList.remove('border-red-500');
this.classList.add('border-gray-300');
}
});
// Form-Submit-Validierung
form.addEventListener('submit', function(e) {
const name = nameInput.value.trim();
const ip = ipInput.value.trim();
const ipRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
if (!name) {
e.preventDefault();
alert('Bitte geben Sie einen Drucker-Namen ein.');
nameInput.focus();
return;
}
if (!ip || !ipRegex.test(ip)) {
e.preventDefault();
alert('Bitte geben Sie eine gültige IP-Adresse ein.');
ipInput.focus();
return;
}
});
// Event-Listener für Drucker-Aktions-Buttons
document.querySelectorAll('.printer-action-btn').forEach(button => {
button.addEventListener('click', function() {
const printerId = this.getAttribute('data-printer-id');
const action = this.getAttribute('data-action');
if (action === 'test') {
testPrinterConnection(printerId, this);
} else if (action === 'toggle') {
togglePrinterPower(printerId, this);
}
});
});
});
// Verbindungstest
function testPrinterConnection(printerId, button) {
const originalText = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Teste...';
button.disabled = true;
fetch(`/api/admin/printers/${printerId}/test-tapo`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
}
})
.then(response => response.json())
.then(data => {
if (data.tapo_test && data.tapo_test.success) {
alert('✅ Verbindung erfolgreich!\n\nStatus: ' + (data.tapo_test.device_info ? data.tapo_test.device_info.device_on ? 'EIN' : 'AUS' : 'Unbekannt'));
} else {
alert('❌ Verbindung fehlgeschlagen!\n\nFehler: ' + (data.tapo_test ? data.tapo_test.error : 'Unbekannter Fehler'));
}
})
.catch(error => {
alert('❌ Verbindungstest fehlgeschlagen!\n\nFehler: ' + error.message);
})
.finally(() => {
button.innerHTML = originalText;
button.disabled = false;
});
}
// Drucker ein/ausschalten
function togglePrinterPower(printerId, button) {
const originalText = button.innerHTML;
if (!confirm('Möchten Sie den Drucker ein-/ausschalten?')) {
return;
}
button.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Schaltet...';
button.disabled = true;
fetch(`/api/admin/printers/${printerId}/toggle`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('✅ Drucker erfolgreich ' + data.action + '!');
// Seite neu laden um aktuellen Status zu zeigen
location.reload();
} else {
alert('❌ Fehler beim Schalten!\n\nFehler: ' + (data.error || 'Unbekannter Fehler'));
}
})
.catch(error => {
alert('❌ Schaltvorgang fehlgeschlagen!\n\nFehler: ' + error.message);
})
.finally(() => {
button.innerHTML = originalText;
button.disabled = false;
});
}
</script>
</body>
</html>

View File

@ -332,6 +332,7 @@
<div class="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-slate-900 dark:border-t-slate-700"></div>
</div>
</div>
{% if current_user.is_authenticated %}
<!-- Benachrichtigungen - kompakteres Design -->
<div class="relative">