🎉 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:
2025-06-02 14:57:58 +02:00
parent 7bea427bd6
commit 6ff407a895
29 changed files with 3148 additions and 450 deletions

View File

@@ -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 %}