🎉 Refactor and optimize database files, enhance error handling with new utility scripts 📚, and update documentation on fault tolerance and unattended operation. 🚀
This commit is contained in:
@ -524,6 +524,36 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white/60 dark:bg-slate-700/60 backdrop-blur-sm rounded-xl border border-slate-200 dark:border-slate-600 p-6 shadow-lg">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">System-Steuerung</h3>
|
||||
<div class="space-y-3">
|
||||
<button id="kiosk-restart-btn" class="w-full px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors text-sm font-medium flex items-center justify-center space-x-2">
|
||||
<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="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
<span>Kiosk neustarten</span>
|
||||
</button>
|
||||
<button id="restart-system-btn" class="w-full px-4 py-2 bg-orange-500 text-white rounded-lg hover:bg-orange-600 transition-colors text-sm font-medium flex items-center justify-center space-x-2">
|
||||
<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"/>
|
||||
</svg>
|
||||
<span>System neustarten</span>
|
||||
</button>
|
||||
<button id="shutdown-system-btn" class="w-full px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors text-sm font-medium flex items-center justify-center space-x-2">
|
||||
<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="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636m12.728 12.728L18.364 5.636M5.636 18.364l12.728-12.728"/>
|
||||
</svg>
|
||||
<span>System herunterfahren</span>
|
||||
</button>
|
||||
<button id="system-status-btn" class="w-full px-4 py-2 bg-slate-500 text-white rounded-lg hover:bg-slate-600 transition-colors text-sm font-medium flex items-center justify-center space-x-2">
|
||||
<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="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
||||
</svg>
|
||||
<span>System-Status</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white/60 dark:bg-slate-700/60 backdrop-blur-sm rounded-xl border border-slate-200 dark:border-slate-600 p-6 shadow-lg">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">Konfiguration</h3>
|
||||
<div class="space-y-3">
|
||||
@ -533,8 +563,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
<button id="update-printers-btn" class="w-full px-4 py-2 bg-indigo-500 text-white rounded-lg hover:bg-indigo-600 transition-colors text-sm font-medium">
|
||||
Drucker aktualisieren
|
||||
</button>
|
||||
<button id="restart-system-btn" class="w-full px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors text-sm font-medium">
|
||||
System neustarten
|
||||
<button id="error-recovery-toggle-btn" class="w-full px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors text-sm font-medium flex items-center justify-center space-x-2">
|
||||
<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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<span>Fehlerresilienz</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -976,5 +1009,532 @@ function getCsrfToken() {
|
||||
const token = document.querySelector('meta[name="csrf-token"]');
|
||||
return token ? token.getAttribute('content') : '';
|
||||
}
|
||||
|
||||
// ===== ERWEITERTE SYSTEM-CONTROL-FUNKTIONALITÄT =====
|
||||
|
||||
class SystemControlManager {
|
||||
constructor() {
|
||||
this.pendingOperations = new Map();
|
||||
this.lastStatusUpdate = null;
|
||||
this.isInitialized = false;
|
||||
|
||||
this.initializeEventListeners();
|
||||
this.startStatusPolling();
|
||||
this.isInitialized = true;
|
||||
|
||||
console.log('🔧 System-Control-Manager initialisiert');
|
||||
}
|
||||
|
||||
initializeEventListeners() {
|
||||
// Kiosk-Neustart
|
||||
const kioskRestartBtn = document.getElementById('kiosk-restart-btn');
|
||||
if (kioskRestartBtn) {
|
||||
kioskRestartBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.handleKioskRestart();
|
||||
});
|
||||
}
|
||||
|
||||
// System-Neustart
|
||||
const restartBtn = document.getElementById('restart-system-btn');
|
||||
if (restartBtn) {
|
||||
restartBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.handleSystemRestart();
|
||||
});
|
||||
}
|
||||
|
||||
// System-Shutdown
|
||||
const shutdownBtn = document.getElementById('shutdown-system-btn');
|
||||
if (shutdownBtn) {
|
||||
shutdownBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.handleSystemShutdown();
|
||||
});
|
||||
}
|
||||
|
||||
// System-Status
|
||||
const statusBtn = document.getElementById('system-status-btn');
|
||||
if (statusBtn) {
|
||||
statusBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.showSystemStatus();
|
||||
});
|
||||
}
|
||||
|
||||
// Error-Recovery Toggle
|
||||
const errorRecoveryBtn = document.getElementById('error-recovery-toggle-btn');
|
||||
if (errorRecoveryBtn) {
|
||||
errorRecoveryBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.toggleErrorRecovery();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async handleKioskRestart() {
|
||||
const confirmed = await this.confirmOperation(
|
||||
'Kiosk-Neustart',
|
||||
'Möchten Sie das Kiosk-Display neustarten? Dies dauert ca. 10-30 Sekunden.',
|
||||
'Der Kiosk wird neugestartet...'
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/kiosk/restart', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
},
|
||||
body: JSON.stringify({
|
||||
delay_seconds: 10,
|
||||
reason: 'Manueller Kiosk-Neustart über Admin-Panel'
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.success) {
|
||||
showNotification('Kiosk-Neustart geplant', 'success');
|
||||
this.trackOperation(result.operation_id, 'kiosk_restart');
|
||||
} else {
|
||||
showNotification(result.error || 'Fehler beim Kiosk-Neustart', 'error');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Kiosk-Neustart Fehler:', error);
|
||||
showNotification('Fehler beim Kiosk-Neustart: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async handleSystemRestart() {
|
||||
const confirmed = await this.confirmOperation(
|
||||
'System-Neustart',
|
||||
'WARNUNG: Das gesamte System wird neu gestartet! Dies dauert ca. 2-5 Minuten.',
|
||||
'Das System wird neu gestartet...',
|
||||
true
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
// Zusätzliche Verzögerungsabfrage
|
||||
const delayInput = prompt('Verzögerung in Sekunden (10-3600):', '60');
|
||||
if (!delayInput) return;
|
||||
|
||||
const delay = Math.max(10, Math.min(3600, parseInt(delayInput) || 60));
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/system/restart', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
},
|
||||
body: JSON.stringify({
|
||||
delay_seconds: delay,
|
||||
reason: 'Manueller System-Neustart über Admin-Panel'
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.success) {
|
||||
showNotification(`System-Neustart in ${delay} Sekunden geplant`, 'success');
|
||||
this.trackOperation(result.operation_id, 'system_restart');
|
||||
this.showCountdown(delay, 'System-Neustart');
|
||||
} else {
|
||||
showNotification(result.error || 'Fehler beim System-Neustart', 'error');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('System-Neustart Fehler:', error);
|
||||
showNotification('Fehler beim System-Neustart: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async handleSystemShutdown() {
|
||||
const confirmed = await this.confirmOperation(
|
||||
'System-Shutdown',
|
||||
'KRITISCHE WARNUNG: Das System wird komplett heruntergefahren!',
|
||||
'Das System wird heruntergefahren...',
|
||||
true
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
// Doppelte Bestätigung
|
||||
const doubleConfirm = confirm('LETZTE WARNUNG: System wirklich herunterfahren?\nDas System muss danach manuell neu gestartet werden!');
|
||||
if (!doubleConfirm) return;
|
||||
|
||||
// Verzögerungsabfrage
|
||||
const delayInput = prompt('Verzögerung in Sekunden (10-3600):', '30');
|
||||
if (!delayInput) return;
|
||||
|
||||
const delay = Math.max(10, Math.min(3600, parseInt(delayInput) || 30));
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/system/shutdown', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
},
|
||||
body: JSON.stringify({
|
||||
delay_seconds: delay,
|
||||
reason: 'Manueller System-Shutdown über Admin-Panel'
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.success) {
|
||||
showNotification(`System-Shutdown in ${delay} Sekunden geplant`, 'error');
|
||||
this.trackOperation(result.operation_id, 'system_shutdown');
|
||||
this.showCountdown(delay, 'System-Shutdown');
|
||||
} else {
|
||||
showNotification(result.error || 'Fehler beim System-Shutdown', 'error');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('System-Shutdown Fehler:', error);
|
||||
showNotification('Fehler beim System-Shutdown: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async showSystemStatus() {
|
||||
try {
|
||||
showNotification('System-Status wird geladen...', 'info');
|
||||
|
||||
const response = await fetch('/api/admin/system/status', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
}
|
||||
});
|
||||
|
||||
const status = await response.json();
|
||||
|
||||
if (response.ok && status.success) {
|
||||
this.displaySystemStatusModal(status);
|
||||
} else {
|
||||
showNotification('Fehler beim Laden des System-Status', 'error');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('System-Status Fehler:', error);
|
||||
showNotification('Fehler beim System-Status: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async toggleErrorRecovery() {
|
||||
try {
|
||||
// Aktuellen Status abrufen
|
||||
const statusResponse = await fetch('/api/admin/error-recovery/status', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
}
|
||||
});
|
||||
|
||||
const statusData = await statusResponse.json();
|
||||
const currentlyActive = statusData?.statistics?.monitoring_active || false;
|
||||
|
||||
// Toggle-Operation
|
||||
const response = await fetch('/api/admin/error-recovery/toggle', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
},
|
||||
body: JSON.stringify({
|
||||
enable: !currentlyActive
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.success) {
|
||||
showNotification(result.message, 'success');
|
||||
this.updateErrorRecoveryButton(result.monitoring_active);
|
||||
} else {
|
||||
showNotification(result.error || 'Fehler beim Error-Recovery Toggle', 'error');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error-Recovery Toggle Fehler:', error);
|
||||
showNotification('Fehler beim Error-Recovery Toggle: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
updateErrorRecoveryButton(isActive) {
|
||||
const btn = document.getElementById('error-recovery-toggle-btn');
|
||||
if (btn) {
|
||||
if (isActive) {
|
||||
btn.classList.remove('bg-green-500', 'hover:bg-green-600');
|
||||
btn.classList.add('bg-yellow-500', 'hover:bg-yellow-600');
|
||||
btn.querySelector('span').textContent = 'Fehlerresilienz AN';
|
||||
} else {
|
||||
btn.classList.remove('bg-yellow-500', 'hover:bg-yellow-600');
|
||||
btn.classList.add('bg-green-500', 'hover:bg-green-600');
|
||||
btn.querySelector('span').textContent = 'Fehlerresilienz AUS';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async confirmOperation(title, message, processingMessage, isDestructive = false) {
|
||||
const bgColor = isDestructive ? 'bg-red-100 dark:bg-red-900' : 'bg-blue-100 dark:bg-blue-900';
|
||||
const iconColor = isDestructive ? 'text-red-600 dark:text-red-400' : 'text-blue-600 dark:text-blue-400';
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4';
|
||||
|
||||
modal.innerHTML = `
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-2xl max-w-md w-full">
|
||||
<div class="text-center mb-6">
|
||||
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full ${bgColor} mb-4">
|
||||
<svg class="h-6 w-6 ${iconColor}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 15.5c-.77.833.192 2.5 1.732 2.5z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-slate-900 dark:text-white mb-2">${title}</h3>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">${message}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-3">
|
||||
<button id="modal-cancel" class="flex-1 px-4 py-2 bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-xl hover:bg-slate-300 dark:hover:bg-slate-600 transition-colors">
|
||||
Abbrechen
|
||||
</button>
|
||||
<button id="modal-confirm" class="flex-1 px-4 py-2 ${isDestructive ? 'bg-red-500 hover:bg-red-600' : 'bg-blue-500 hover:bg-blue-600'} text-white rounded-xl transition-colors">
|
||||
Bestätigen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
const cancelBtn = modal.querySelector('#modal-cancel');
|
||||
const confirmBtn = modal.querySelector('#modal-confirm');
|
||||
|
||||
cancelBtn.addEventListener('click', () => {
|
||||
document.body.removeChild(modal);
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
confirmBtn.addEventListener('click', () => {
|
||||
document.body.removeChild(modal);
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
// ESC-Key
|
||||
const handleEsc = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
document.body.removeChild(modal);
|
||||
document.removeEventListener('keydown', handleEsc);
|
||||
resolve(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleEsc);
|
||||
});
|
||||
}
|
||||
|
||||
showCountdown(seconds, operationType) {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'fixed inset-0 bg-black/75 backdrop-blur-sm z-50 flex items-center justify-center p-4';
|
||||
|
||||
modal.innerHTML = `
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl p-8 shadow-2xl max-w-sm w-full text-center">
|
||||
<div class="mx-auto flex items-center justify-center h-16 w-16 rounded-full bg-orange-100 dark:bg-orange-900 mb-6">
|
||||
<svg class="h-8 w-8 text-orange-600 dark:text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-slate-900 dark:text-white mb-2">${operationType}</h3>
|
||||
<p class="text-slate-600 dark:text-slate-400 mb-6">Wird ausgeführt in:</p>
|
||||
<div id="countdown-timer" class="text-4xl font-bold text-orange-600 dark:text-orange-400 mb-6">${seconds}</div>
|
||||
<button id="countdown-cancel" class="px-6 py-2 bg-red-500 text-white rounded-xl hover:bg-red-600 transition-colors">
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
const timerEl = modal.querySelector('#countdown-timer');
|
||||
const cancelBtn = modal.querySelector('#countdown-cancel');
|
||||
|
||||
let remainingSeconds = seconds;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
remainingSeconds--;
|
||||
timerEl.textContent = remainingSeconds;
|
||||
|
||||
if (remainingSeconds <= 0) {
|
||||
clearInterval(interval);
|
||||
document.body.removeChild(modal);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
cancelBtn.addEventListener('click', async () => {
|
||||
clearInterval(interval);
|
||||
document.body.removeChild(modal);
|
||||
|
||||
// Versuche Operation zu stornieren
|
||||
try {
|
||||
// Hier könnte der API-Call zum Stornieren implementiert werden
|
||||
showNotification('Operation wird storniert...', 'info');
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Stornieren:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
displaySystemStatusModal(status) {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4';
|
||||
|
||||
const servicesHtml = Object.entries(status.services || {}).map(([name, state]) => {
|
||||
const isActive = state === 'active';
|
||||
return `
|
||||
<div class="flex items-center justify-between py-2 px-3 rounded-lg ${isActive ? 'bg-green-50 dark:bg-green-900/20' : 'bg-red-50 dark:bg-red-900/20'}">
|
||||
<span class="font-medium">${name}</span>
|
||||
<span class="px-2 py-1 rounded-full text-xs ${isActive ? 'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100' : 'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100'}">${state}</span>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
const metrics = status.system_metrics || {};
|
||||
const errorRecovery = status.error_recovery || {};
|
||||
|
||||
modal.innerHTML = `
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-2xl max-w-2xl w-full max-h-90vh overflow-y-auto">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-xl font-bold text-slate-900 dark:text-white">System-Status</h3>
|
||||
<button id="close-status-modal" 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"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Services -->
|
||||
<div>
|
||||
<h4 class="font-semibold text-slate-900 dark:text-white mb-3">Services</h4>
|
||||
<div class="space-y-2">
|
||||
${servicesHtml}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System-Metriken -->
|
||||
<div>
|
||||
<h4 class="font-semibold text-slate-900 dark:text-white mb-3">System-Metriken</h4>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between">
|
||||
<span>Speicher:</span>
|
||||
<span class="font-medium ${metrics.memory_percent > 80 ? 'text-red-600' : 'text-green-600'}">${metrics.memory_percent?.toFixed(1) || 0}%</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>Festplatte:</span>
|
||||
<span class="font-medium ${metrics.disk_percent > 90 ? 'text-red-600' : 'text-green-600'}">${metrics.disk_percent?.toFixed(1) || 0}%</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>System-Last:</span>
|
||||
<span class="font-medium">${metrics.load_average?.toFixed(2) || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fehlerresilienz -->
|
||||
<div class="md:col-span-2">
|
||||
<h4 class="font-semibold text-slate-900 dark:text-white mb-3">Fehlerresilienz</h4>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div class="text-center p-3 bg-slate-50 dark:bg-slate-700 rounded-lg">
|
||||
<div class="text-2xl font-bold text-blue-600">${errorRecovery.total_errors || 0}</div>
|
||||
<div class="text-xs text-slate-600 dark:text-slate-400">Gesamt-Fehler</div>
|
||||
</div>
|
||||
<div class="text-center p-3 bg-slate-50 dark:bg-slate-700 rounded-lg">
|
||||
<div class="text-2xl font-bold text-orange-600">${errorRecovery.errors_last_24h || 0}</div>
|
||||
<div class="text-xs text-slate-600 dark:text-slate-400">Letzten 24h</div>
|
||||
</div>
|
||||
<div class="text-center p-3 bg-slate-50 dark:bg-slate-700 rounded-lg">
|
||||
<div class="text-2xl font-bold text-green-600">${errorRecovery.recovery_success_rate || 0}%</div>
|
||||
<div class="text-xs text-slate-600 dark:text-slate-400">Erfolgsrate</div>
|
||||
</div>
|
||||
<div class="text-center p-3 bg-slate-50 dark:bg-slate-700 rounded-lg">
|
||||
<div class="text-2xl font-bold ${status.is_safe ? 'text-green-600' : 'text-red-600'}">${status.is_safe ? 'SICHER' : 'UNSICHER'}</div>
|
||||
<div class="text-xs text-slate-600 dark:text-slate-400">Status</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 pt-4 border-t border-slate-200 dark:border-slate-600">
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400">
|
||||
Letztes Update: ${new Date().toLocaleString('de-DE')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
const closeBtn = modal.querySelector('#close-status-modal');
|
||||
closeBtn.addEventListener('click', () => {
|
||||
document.body.removeChild(modal);
|
||||
});
|
||||
|
||||
// ESC zum Schließen
|
||||
const handleEsc = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
document.body.removeChild(modal);
|
||||
document.removeEventListener('keydown', handleEsc);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleEsc);
|
||||
}
|
||||
|
||||
trackOperation(operationId, type) {
|
||||
this.pendingOperations.set(operationId, {
|
||||
id: operationId,
|
||||
type: type,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
async startStatusPolling() {
|
||||
// Überwache Error-Recovery-Status
|
||||
setInterval(async () => {
|
||||
try {
|
||||
const response = await fetch('/api/admin/error-recovery/status', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
this.updateErrorRecoveryButton(data?.statistics?.monitoring_active || false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.debug('Status-Polling Fehler:', error);
|
||||
}
|
||||
}, 30000); // Alle 30 Sekunden
|
||||
}
|
||||
}
|
||||
|
||||
// Globaler System-Control-Manager
|
||||
let systemControlManager = null;
|
||||
|
||||
// Initialisierung beim DOM-Laden
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!systemControlManager) {
|
||||
systemControlManager = new SystemControlManager();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@ -13,207 +13,124 @@
|
||||
|
||||
<style>
|
||||
.calendar-container {
|
||||
@apply bg-white dark:bg-gray-800 rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 p-6 mb-6 transition-all duration-300;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.fc-event {
|
||||
font-size: 11px;
|
||||
border-radius: 6px;
|
||||
padding: 2px 6px;
|
||||
border: none !important;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.fc-event-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* FullCalendar Dark Mode */
|
||||
.dark .fc-theme-standard td,
|
||||
.dark .fc-theme-standard th {
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
.dark .fc-theme-standard .fc-scrollgrid {
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
.dark .fc-col-header-cell {
|
||||
background: #1f2937;
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
.dark .fc-daygrid-day {
|
||||
background: #1f2937;
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
.dark .fc-day-today {
|
||||
background-color: #065f46 !important;
|
||||
}
|
||||
|
||||
.dark .fc-button-primary {
|
||||
background: #3b82f6;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.dark .fc-button-primary:hover {
|
||||
background: #2563eb;
|
||||
border-color: #2563eb;
|
||||
}
|
||||
|
||||
.dark .fc-toolbar-title {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
@apply bg-gradient-to-br from-blue-500 via-blue-600 to-indigo-700 dark:from-blue-600 dark:via-blue-700 dark:to-indigo-800 rounded-xl p-6 text-white shadow-lg border border-blue-200 dark:border-blue-800 transition-all duration-300 hover:shadow-xl hover:scale-105;
|
||||
}
|
||||
|
||||
.stats-card-success {
|
||||
@apply bg-gradient-to-br from-emerald-500 via-emerald-600 to-green-700 dark:from-emerald-600 dark:via-emerald-700 dark:to-green-800;
|
||||
}
|
||||
|
||||
.stats-card-warning {
|
||||
@apply bg-gradient-to-br from-amber-500 via-orange-500 to-orange-600 dark:from-amber-600 dark:via-orange-600 dark:to-orange-700;
|
||||
}
|
||||
|
||||
.stats-card-danger {
|
||||
@apply bg-gradient-to-br from-red-500 via-red-600 to-rose-700 dark:from-red-600 dark:via-red-700 dark:to-rose-800;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
color: white;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
@apply bg-white dark:bg-gray-800 rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 p-6 mb-6 transition-all duration-300;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
@apply flex gap-4 items-center flex-wrap;
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn-calendar {
|
||||
@apply bg-blue-600 hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-600 text-white border-none px-4 py-2 rounded-lg cursor-pointer transition-all duration-200 font-medium shadow-md hover:shadow-lg;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.btn-calendar:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.btn-calendar.active {
|
||||
@apply bg-blue-800 dark:bg-blue-500 shadow-lg scale-105;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
@apply bg-red-600 hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-600 text-white px-4 py-2 rounded-lg transition-all duration-200 font-medium shadow-md hover:shadow-lg;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@apply bg-blue-600 hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-600 text-white px-4 py-2 rounded-lg transition-all duration-200 font-medium shadow-md hover:shadow-lg;
|
||||
background: #1d4ed8;
|
||||
}
|
||||
|
||||
.legend {
|
||||
@apply flex gap-6 flex-wrap mt-4 pt-4 border-t border-gray-200 dark:border-gray-600;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
@apply flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
@apply w-4 h-4 rounded;
|
||||
}
|
||||
|
||||
.select-custom {
|
||||
@apply border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
@apply bg-gradient-to-r from-blue-50 via-indigo-50 to-purple-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900 border-b border-gray-200 dark:border-gray-700;
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
@apply fixed inset-0 bg-black bg-opacity-50 dark:bg-opacity-70 z-50 transition-opacity duration-300;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
@apply bg-white dark:bg-gray-800 rounded-xl max-w-2xl w-full p-6 shadow-2xl border border-gray-200 dark:border-gray-700 transition-all duration-300;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
@apply flex justify-between items-center mb-6 pb-4 border-b border-gray-200 dark:border-gray-600;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@apply text-xl font-semibold text-gray-900 dark:text-gray-100;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
@apply text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition-colors duration-200 text-xl;
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
@apply grid grid-cols-1 md:grid-cols-2 gap-4 text-sm;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
@apply space-y-1;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
@apply font-medium text-gray-600 dark:text-gray-400;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
@apply text-gray-900 dark:text-gray-100 bg-gray-50 dark:bg-gray-700 px-3 py-2 rounded-lg;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
@apply bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-400 p-3 rounded-lg border border-red-200 dark:border-red-800;
|
||||
}
|
||||
|
||||
.notes-section {
|
||||
@apply bg-blue-50 dark:bg-blue-900/20 text-blue-800 dark:text-blue-300 p-3 rounded-lg border border-blue-200 dark:border-blue-800;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 transition-colors duration-300">
|
||||
<div class="min-h-screen">
|
||||
<!-- Header mit Breadcrumb -->
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="py-8">
|
||||
<nav class="text-sm font-medium text-gray-600 dark:text-gray-400 mb-6">
|
||||
<div class="py-6">
|
||||
<nav class="text-sm font-medium text-gray-600 mb-4">
|
||||
<ol class="list-none p-0 inline-flex">
|
||||
<li class="flex items-center">
|
||||
<a href="{{ url_for('admin_page') }}" class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200">
|
||||
<i class="fas fa-tachometer-alt mr-2"></i>
|
||||
Admin-Dashboard
|
||||
</a>
|
||||
<svg class="fill-current w-4 h-4 mx-3 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
|
||||
<path d="M285.476 272.971c4.686 4.686 4.686 12.284 0 16.97l-133.952 133.954c-4.686 4.686-12.284 4.686-16.97 0l-133.952-133.954c-4.686-4.686-4.686-12.284 0-16.97 4.686-4.686 12.284-4.686 16.97 0l125.462 125.463 125.462-125.463c4.686-4.686 12.284-4.686 16.97 0z"/>
|
||||
<a href="{{ url_for('admin_page') }}" class="hover:text-blue-600">Admin-Dashboard</a>
|
||||
<svg class="fill-current w-3 h-3 mx-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
|
||||
<path d="m285.476 272.971c4.686 4.686 4.686 12.284 0 16.97l-133.952 133.954c-4.686 4.686-12.284 4.686-16.97 0l-133.952-133.954c-4.686-4.686-4.686-12.284 0-16.97 4.686-4.686 12.284-4.686 16.97 0l125.462 125.463 125.462-125.463c4.686-4.686 12.284-4.686 16.97 0z"/>
|
||||
</svg>
|
||||
</li>
|
||||
<li class="text-gray-900 dark:text-gray-100 font-semibold">
|
||||
<i class="fas fa-plug mr-2"></i>
|
||||
<li class="text-gray-900 font-semibold">
|
||||
Steckdosenschaltzeiten
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="flex flex-col lg:flex-row lg:justify-between lg:items-center gap-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="text-4xl font-bold text-gray-900 dark:text-gray-100 mb-2">
|
||||
<i class="fas fa-plug text-blue-600 dark:text-blue-400 mr-4"></i>
|
||||
<h1 class="text-3xl font-bold text-gray-900">
|
||||
<i class="fas fa-plug text-blue-600 mr-3"></i>
|
||||
Steckdosenschaltzeiten
|
||||
</h1>
|
||||
<p class="text-lg text-gray-600 dark:text-gray-400">
|
||||
<p class="mt-2 text-gray-600">
|
||||
Kalenderübersicht aller Drucker-Steckdosenschaltungen mit detaillierter Analyse
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<button id="refreshData" class="btn-primary">
|
||||
<button id="refreshData" class="btn-calendar">
|
||||
<i class="fas fa-sync-alt mr-2"></i>
|
||||
Aktualisieren
|
||||
</button>
|
||||
|
||||
<button id="cleanupLogs" class="btn-danger">
|
||||
<button id="cleanupLogs" class="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700">
|
||||
<i class="fas fa-trash mr-2"></i>
|
||||
Alte Logs löschen
|
||||
</button>
|
||||
@ -224,64 +141,52 @@
|
||||
</div>
|
||||
|
||||
<!-- Haupt-Container -->
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-8">
|
||||
<!-- Statistik-Karten -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6">
|
||||
<div class="stats-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm opacity-90 mb-1">Schaltungen (24h)</p>
|
||||
<p class="text-3xl font-bold" id="totalLogs">{{ stats.total_logs or 0 }}</p>
|
||||
<p class="text-xs opacity-80 mt-1">Gesamt-Events</p>
|
||||
</div>
|
||||
<div class="p-3 bg-white/20 rounded-xl">
|
||||
<i class="fas fa-chart-line text-2xl"></i>
|
||||
<p class="text-sm opacity-80">Schaltungen (24h)</p>
|
||||
<p class="text-2xl font-bold" id="totalLogs">{{ stats.total_logs or 0 }}</p>
|
||||
</div>
|
||||
<i class="fas fa-chart-line text-2xl opacity-60"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-card stats-card-success">
|
||||
<div class="stats-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm opacity-90 mb-1">Erfolgsrate</p>
|
||||
<p class="text-3xl font-bold" id="successRate">{{ "%.1f"|format(100 - stats.error_rate) }}%</p>
|
||||
<p class="text-xs opacity-80 mt-1">Erfolgreiche Schaltungen</p>
|
||||
</div>
|
||||
<div class="p-3 bg-white/20 rounded-xl">
|
||||
<i class="fas fa-check-circle text-2xl"></i>
|
||||
<p class="text-sm opacity-80">Erfolgsrate</p>
|
||||
<p class="text-2xl font-bold" id="successRate">{{ "%.1f"|format(100 - stats.error_rate) }}%</p>
|
||||
</div>
|
||||
<i class="fas fa-check-circle text-2xl opacity-60"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-card stats-card-warning">
|
||||
<div class="stats-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm opacity-90 mb-1">Ø Antwortzeit</p>
|
||||
<p class="text-3xl font-bold" id="avgResponseTime">
|
||||
<p class="text-sm opacity-80">Ø Antwortzeit</p>
|
||||
<p class="text-2xl font-bold" id="avgResponseTime">
|
||||
{% if stats.average_response_time_ms %}
|
||||
{{ "%.0f"|format(stats.average_response_time_ms) }}ms
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="text-xs opacity-80 mt-1">Durchschnittlich</p>
|
||||
</div>
|
||||
<div class="p-3 bg-white/20 rounded-xl">
|
||||
<i class="fas fa-stopwatch text-2xl"></i>
|
||||
</div>
|
||||
<i class="fas fa-stopwatch text-2xl opacity-60"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-card stats-card-danger">
|
||||
<div class="stats-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm opacity-90 mb-1">Fehlerzahl</p>
|
||||
<p class="text-3xl font-bold" id="errorCount">{{ stats.error_count or 0 }}</p>
|
||||
<p class="text-xs opacity-80 mt-1">Gescheiterte Versuche</p>
|
||||
</div>
|
||||
<div class="p-3 bg-white/20 rounded-xl">
|
||||
<i class="fas fa-exclamation-triangle text-2xl"></i>
|
||||
<p class="text-sm opacity-80">Fehlerzahl</p>
|
||||
<p class="text-2xl font-bold" id="errorCount">{{ stats.error_count or 0 }}</p>
|
||||
</div>
|
||||
<i class="fas fa-exclamation-triangle text-2xl opacity-60"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -289,62 +194,39 @@
|
||||
<!-- Filter und Steuerung -->
|
||||
<div class="control-panel">
|
||||
<div class="filter-group">
|
||||
<div class="flex items-center gap-3">
|
||||
<label for="printerFilter" class="font-medium text-gray-700 dark:text-gray-300">
|
||||
<i class="fas fa-filter mr-2"></i>
|
||||
Drucker filtern:
|
||||
</label>
|
||||
<select id="printerFilter" class="select-custom">
|
||||
<option value="">Alle Drucker anzeigen</option>
|
||||
{% for printer in printers %}
|
||||
<option value="{{ printer.id }}">{{ printer.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<label for="printerFilter" class="font-medium text-gray-700">Drucker filtern:</label>
|
||||
<select id="printerFilter" class="border border-gray-300 rounded-lg px-3 py-2 bg-white">
|
||||
<option value="">Alle Drucker</option>
|
||||
{% for printer in printers %}
|
||||
<option value="{{ printer.id }}">{{ printer.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<div class="flex items-center gap-3 border-l border-gray-300 dark:border-gray-600 pl-6 ml-6">
|
||||
<label class="font-medium text-gray-700 dark:text-gray-300">
|
||||
<i class="fas fa-eye mr-2"></i>
|
||||
Ansicht:
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<button id="monthView" class="btn-calendar active">
|
||||
<i class="fas fa-calendar-alt mr-1"></i>
|
||||
Monat
|
||||
</button>
|
||||
<button id="weekView" class="btn-calendar">
|
||||
<i class="fas fa-calendar-week mr-1"></i>
|
||||
Woche
|
||||
</button>
|
||||
<button id="dayView" class="btn-calendar">
|
||||
<i class="fas fa-calendar-day mr-1"></i>
|
||||
Tag
|
||||
</button>
|
||||
</div>
|
||||
<div class="border-l border-gray-300 pl-4 ml-4">
|
||||
<label class="font-medium text-gray-700 mr-3">Ansicht:</label>
|
||||
<button id="monthView" class="btn-calendar active">Monat</button>
|
||||
<button id="weekView" class="btn-calendar">Woche</button>
|
||||
<button id="dayView" class="btn-calendar">Tag</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Legende -->
|
||||
<div class="legend">
|
||||
<h4 class="font-medium text-gray-700 dark:text-gray-300 w-full mb-2">
|
||||
<i class="fas fa-info-circle mr-2"></i>
|
||||
Status-Legende:
|
||||
</h4>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color bg-green-500"></div>
|
||||
<span><i class="fas fa-toggle-on mr-1"></i>Steckdose EIN</span>
|
||||
<div class="legend-color" style="background-color: #10b981;"></div>
|
||||
<span>Steckdose EIN</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color bg-orange-500"></div>
|
||||
<span><i class="fas fa-toggle-off mr-1"></i>Steckdose AUS</span>
|
||||
<div class="legend-color" style="background-color: #f59e0b;"></div>
|
||||
<span>Steckdose AUS</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color bg-blue-500"></div>
|
||||
<span><i class="fas fa-plug mr-1"></i>Verbunden</span>
|
||||
<div class="legend-color" style="background-color: #3b82f6;"></div>
|
||||
<span>Verbunden</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color bg-red-500"></div>
|
||||
<span><i class="fas fa-unlink mr-1"></i>Getrennt</span>
|
||||
<div class="legend-color" style="background-color: #ef4444;"></div>
|
||||
<span>Getrennt</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -355,15 +237,12 @@
|
||||
</div>
|
||||
|
||||
<!-- Detailansicht Modal -->
|
||||
<div id="eventDetailModal" class="modal-overlay hidden">
|
||||
<div id="eventDetailModal" 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="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">
|
||||
<i class="fas fa-info-circle mr-2 text-blue-600 dark:text-blue-400"></i>
|
||||
Schaltung Details
|
||||
</h3>
|
||||
<button id="closeModal" class="close-button">
|
||||
<div class="bg-white rounded-lg max-w-lg w-full p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold">Schaltung Details</h3>
|
||||
<button id="closeModal" class="text-gray-400 hover:text-gray-600">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
@ -372,9 +251,8 @@
|
||||
<!-- Wird dynamisch gefüllt -->
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end gap-3">
|
||||
<button id="closeModalBtn" class="px-6 py-2 bg-gray-300 dark:bg-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-400 dark:hover:bg-gray-500 transition-all duration-200 font-medium">
|
||||
<i class="fas fa-times mr-2"></i>
|
||||
<div class="mt-6 flex justify-end">
|
||||
<button id="closeModalBtn" class="px-4 py-2 bg-gray-300 text-gray-700 rounded-lg hover:bg-gray-400">
|
||||
Schließen
|
||||
</button>
|
||||
</div>
|
||||
@ -414,17 +292,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
eventDidMount: function(info) {
|
||||
// Tooltip hinzufügen
|
||||
info.el.title = info.event.title + '\nKlicken für Details';
|
||||
|
||||
// Hover-Effekt
|
||||
info.el.addEventListener('mouseenter', function() {
|
||||
this.style.transform = 'scale(1.05)';
|
||||
this.style.zIndex = '10';
|
||||
});
|
||||
|
||||
info.el.addEventListener('mouseleave', function() {
|
||||
this.style.transform = 'scale(1)';
|
||||
this.style.zIndex = '1';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -459,108 +326,78 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const modal = document.getElementById('eventDetailModal');
|
||||
const content = document.getElementById('modalContent');
|
||||
|
||||
const startTime = new Date(event.start).toLocaleString('de-DE', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
const startTime = new Date(event.start).toLocaleString('de-DE');
|
||||
|
||||
let detailsHtml = `
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center p-4 bg-gray-50 dark:bg-gray-700 rounded-xl">
|
||||
<span class="w-6 h-6 rounded-lg mr-4 shadow-md" style="background-color: ${event.backgroundColor}"></span>
|
||||
<div>
|
||||
<h4 class="font-semibold text-lg text-gray-900 dark:text-gray-100">${event.title}</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">${startTime}</p>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center">
|
||||
<span class="w-4 h-4 rounded mr-3" style="background-color: ${event.backgroundColor}"></span>
|
||||
<span class="font-medium">${event.title}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">
|
||||
<i class="fas fa-printer mr-1"></i>
|
||||
Drucker:
|
||||
</div>
|
||||
<div class="detail-value">${props.printer_name}</div>
|
||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span class="font-medium text-gray-600">Zeitpunkt:</span>
|
||||
<p>${startTime}</p>
|
||||
</div>
|
||||
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">
|
||||
<i class="fas fa-info-circle mr-1"></i>
|
||||
Status:
|
||||
</div>
|
||||
<div class="detail-value capitalize">${props.status}</div>
|
||||
<div>
|
||||
<span class="font-medium text-gray-600">Drucker:</span>
|
||||
<p>${props.printer_name}</p>
|
||||
</div>
|
||||
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">
|
||||
<i class="fas fa-source mr-1"></i>
|
||||
Quelle:
|
||||
</div>
|
||||
<div class="detail-value capitalize">${props.source}</div>
|
||||
<div>
|
||||
<span class="font-medium text-gray-600">Status:</span>
|
||||
<p class="capitalize">${props.status}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="font-medium text-gray-600">Quelle:</span>
|
||||
<p class="capitalize">${props.source}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (props.user_name) {
|
||||
detailsHtml += `
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">
|
||||
<i class="fas fa-user mr-1"></i>
|
||||
Benutzer:
|
||||
</div>
|
||||
<div class="detail-value">${props.user_name}</div>
|
||||
<div>
|
||||
<span class="font-medium text-gray-600">Benutzer:</span>
|
||||
<p>${props.user_name}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (props.response_time_ms) {
|
||||
detailsHtml += `
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">
|
||||
<i class="fas fa-clock mr-1"></i>
|
||||
Antwortzeit:
|
||||
</div>
|
||||
<div class="detail-value">${props.response_time_ms}ms</div>
|
||||
<div>
|
||||
<span class="font-medium text-gray-600">Antwortzeit:</span>
|
||||
<p>${props.response_time_ms}ms</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (props.power_consumption) {
|
||||
detailsHtml += `
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">
|
||||
<i class="fas fa-bolt mr-1"></i>
|
||||
Verbrauch:
|
||||
</div>
|
||||
<div class="detail-value">${props.power_consumption}W</div>
|
||||
<div>
|
||||
<span class="font-medium text-gray-600">Verbrauch:</span>
|
||||
<p>${props.power_consumption}W</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (props.voltage) {
|
||||
detailsHtml += `
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">
|
||||
<i class="fas fa-tachometer-alt mr-1"></i>
|
||||
Spannung:
|
||||
</div>
|
||||
<div class="detail-value">${props.voltage}V</div>
|
||||
<div>
|
||||
<span class="font-medium text-gray-600">Spannung:</span>
|
||||
<p>${props.voltage}V</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (props.current) {
|
||||
detailsHtml += `
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">
|
||||
<i class="fas fa-wave-square mr-1"></i>
|
||||
Strom:
|
||||
</div>
|
||||
<div class="detail-value">${props.current}A</div>
|
||||
<div>
|
||||
<span class="font-medium text-gray-600">Strom:</span>
|
||||
<p>${props.current}A</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -569,78 +406,39 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
if (props.notes) {
|
||||
detailsHtml += `
|
||||
<div class="mt-6">
|
||||
<div class="notes-section">
|
||||
<div class="flex items-start gap-2">
|
||||
<i class="fas fa-sticky-note mt-0.5"></i>
|
||||
<div>
|
||||
<span class="font-medium">Notizen:</span>
|
||||
<p class="mt-1">${props.notes}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<span class="font-medium text-gray-600">Notizen:</span>
|
||||
<p class="text-sm bg-gray-50 p-2 rounded mt-1">${props.notes}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (props.error_message) {
|
||||
detailsHtml += `
|
||||
<div class="mt-6">
|
||||
<div class="error-message">
|
||||
<div class="flex items-start gap-2">
|
||||
<i class="fas fa-exclamation-triangle mt-0.5"></i>
|
||||
<div>
|
||||
<span class="font-medium">Fehlermeldung:</span>
|
||||
<p class="mt-1">${props.error_message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<span class="font-medium text-red-600">Fehlermeldung:</span>
|
||||
<p class="text-sm bg-red-50 text-red-700 p-2 rounded mt-1">${props.error_message}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
content.innerHTML = detailsHtml;
|
||||
modal.classList.remove('hidden');
|
||||
|
||||
// Animation
|
||||
setTimeout(() => {
|
||||
modal.style.opacity = '1';
|
||||
modal.querySelector('.modal-content').style.transform = 'scale(1)';
|
||||
}, 10);
|
||||
}
|
||||
|
||||
// Modal schließen
|
||||
function closeModal() {
|
||||
const modal = document.getElementById('eventDetailModal');
|
||||
modal.style.opacity = '0';
|
||||
modal.querySelector('.modal-content').style.transform = 'scale(0.95)';
|
||||
|
||||
setTimeout(() => {
|
||||
modal.classList.add('hidden');
|
||||
}, 300);
|
||||
document.getElementById('eventDetailModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
// Event-Listener
|
||||
document.getElementById('closeModal').addEventListener('click', closeModal);
|
||||
document.getElementById('closeModalBtn').addEventListener('click', closeModal);
|
||||
|
||||
// Modal schließen bei Klick außerhalb
|
||||
document.getElementById('eventDetailModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Drucker-Filter
|
||||
document.getElementById('printerFilter').addEventListener('change', function() {
|
||||
currentPrinterFilter = this.value;
|
||||
calendar.refetchEvents();
|
||||
|
||||
// Visual Feedback
|
||||
this.style.transform = 'scale(1.02)';
|
||||
setTimeout(() => {
|
||||
this.style.transform = 'scale(1)';
|
||||
}, 150);
|
||||
});
|
||||
|
||||
// Ansicht-Buttons
|
||||
@ -668,29 +466,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// Aktualisieren-Button
|
||||
document.getElementById('refreshData').addEventListener('click', function() {
|
||||
// Loading-Animation
|
||||
const originalContent = this.innerHTML;
|
||||
this.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Lädt...';
|
||||
this.disabled = true;
|
||||
|
||||
calendar.refetchEvents();
|
||||
loadStatistics();
|
||||
|
||||
// Reset nach 2 Sekunden
|
||||
setTimeout(() => {
|
||||
this.innerHTML = originalContent;
|
||||
this.disabled = false;
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
// Cleanup-Button
|
||||
document.getElementById('cleanupLogs').addEventListener('click', function() {
|
||||
if (confirm('Möchten Sie wirklich alte Logs löschen? (älter als 30 Tage)\n\nDieser Vorgang kann nicht rückgängig gemacht werden.')) {
|
||||
// Loading-Animation
|
||||
const originalContent = this.innerHTML;
|
||||
this.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Lösche...';
|
||||
this.disabled = true;
|
||||
|
||||
if (confirm('Möchten Sie wirklich alte Logs löschen? (älter als 30 Tage)')) {
|
||||
fetch('/api/admin/plug-schedules/cleanup', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@ -702,20 +484,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(`✅ Erfolgreich ${data.deleted_count} alte Einträge gelöscht`);
|
||||
alert(`Erfolgreich ${data.deleted_count} alte Einträge gelöscht`);
|
||||
calendar.refetchEvents();
|
||||
loadStatistics();
|
||||
} else {
|
||||
alert('❌ Fehler beim Löschen: ' + data.error);
|
||||
alert('Fehler beim Löschen: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler:', error);
|
||||
alert('❌ Fehler beim Löschen der Logs');
|
||||
})
|
||||
.finally(() => {
|
||||
this.innerHTML = originalContent;
|
||||
this.disabled = false;
|
||||
alert('Fehler beim Löschen der Logs');
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -727,15 +505,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const stats = data.statistics;
|
||||
|
||||
// Animierte Zahlen-Updates
|
||||
animateValue('totalLogs', stats.total_logs || 0);
|
||||
animateValue('successRate', (100 - (stats.error_rate || 0)), '%');
|
||||
animateValue('errorCount', stats.error_count || 0);
|
||||
|
||||
const avgTime = stats.average_response_time_ms;
|
||||
document.getElementById('totalLogs').textContent = stats.total_logs || 0;
|
||||
document.getElementById('successRate').textContent = (100 - (stats.error_rate || 0)).toFixed(1) + '%';
|
||||
document.getElementById('avgResponseTime').textContent =
|
||||
avgTime ? Math.round(avgTime) + 'ms' : 'N/A';
|
||||
stats.average_response_time_ms ? Math.round(stats.average_response_time_ms) + 'ms' : 'N/A';
|
||||
document.getElementById('errorCount').textContent = stats.error_count || 0;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
@ -743,37 +517,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
}
|
||||
|
||||
// Animierte Werte-Updates
|
||||
function animateValue(elementId, endValue, suffix = '') {
|
||||
const element = document.getElementById(elementId);
|
||||
const startValue = parseInt(element.textContent) || 0;
|
||||
const difference = endValue - startValue;
|
||||
const duration = 1000; // 1 Sekunde
|
||||
const steps = 30;
|
||||
const stepValue = difference / steps;
|
||||
const stepDuration = duration / steps;
|
||||
|
||||
let currentValue = startValue;
|
||||
let currentStep = 0;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
currentStep++;
|
||||
currentValue += stepValue;
|
||||
|
||||
if (currentStep >= steps) {
|
||||
currentValue = endValue;
|
||||
clearInterval(timer);
|
||||
}
|
||||
|
||||
element.textContent = Math.round(currentValue) + suffix;
|
||||
}, stepDuration);
|
||||
}
|
||||
|
||||
// Kalender initialisieren
|
||||
initCalendar();
|
||||
|
||||
// Initiales Laden der Statistiken
|
||||
loadStatistics();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
Reference in New Issue
Block a user