🔧 Update: Enhanced error handling and logging across various modules

**Änderungen:**
-  app.py: Hinzugefügt, um CSRF-Fehler zu behandeln
-  models.py: Fehlerprotokollierung bei der Suche nach Gastanfragen per OTP
-  api.py: Fehlerprotokollierung beim Markieren von Benachrichtigungen als gelesen
-  calendar.py: Fallback-Daten zurückgeben, wenn keine Kalenderereignisse vorhanden sind
-  guest.py: Status-Check-Seite für Gäste aktualisiert
-  hardware_integration.py: Debugging-Informationen für erweiterte Geräteinformationen hinzugefügt
-  tapo_status_manager.py: Rückgabewert für Statusabfrage hinzugefügt

**Ergebnis:**
- Verbesserte Fehlerbehandlung und Protokollierung für eine robustere Anwendung
- Bessere Nachverfolgbarkeit von Fehlern und Systemverhalten

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-06-15 22:45:20 +02:00
parent 7e156099d5
commit 956c24d8ca
552 changed files with 11252 additions and 2424 deletions

View File

@ -0,0 +1,482 @@
/**
* Unified Dark/Light Mode System für MYP Platform
* ===============================================
*
* Konsolidiert alle Theme-Inkonsistenzen in einer einheitlichen Implementierung
* Ersetzt: input.css, components.css, professional-theme.css Dark Mode-Teile
*
* Author: Till Tomczak - MYP Team
* Zweck: Behebung aller UI Dark/Light Mode Inkonsistenzen
*/
/* ===== UNIFIED CSS VARIABLES SYSTEM ===== */
:root {
/* === MERCEDES-BENZ BRAND COLORS === */
--mb-primary: #0073ce;
--mb-primary-dark: #005a9f;
--mb-primary-light: #1e88e5;
--mb-secondary: #00adef;
--mb-black: #000000;
--mb-white: #ffffff;
--mb-silver: #c4c4c4;
--mb-gray: #666666;
/* === LIGHT MODE FOUNDATION === */
--bg-primary: #ffffff;
--bg-secondary: #fafbfc;
--bg-tertiary: #f8fafc;
--bg-card: #ffffff;
--bg-surface: #fefefe;
--bg-overlay: rgba(255, 255, 255, 0.95);
--bg-modal: rgba(255, 255, 255, 0.98);
/* === TEXT COLORS LIGHT === */
--text-primary: #111827;
--text-secondary: #374151;
--text-tertiary: #6b7280;
--text-muted: #9ca3af;
--text-accent: var(--mb-primary);
--text-on-primary: var(--mb-white);
--text-success: #059669;
--text-warning: #d97706;
--text-error: #dc2626;
/* === BORDER COLORS LIGHT === */
--border-primary: #e5e7eb;
--border-secondary: #d1d5db;
--border-focus: var(--mb-primary);
--border-error: #f87171;
--border-success: #34d399;
/* === SHADOWS LIGHT === */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
--shadow-modal: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
/* === GLASSMORPHISM LIGHT === */
--glass-bg: rgba(255, 255, 255, 0.8);
--glass-border: rgba(255, 255, 255, 0.2);
--glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
--glass-blur: blur(16px);
/* === INTERACTIVE STATES LIGHT === */
--hover-bg: rgba(0, 115, 206, 0.04);
--active-bg: rgba(0, 115, 206, 0.08);
--focus-ring: 0 0 0 3px rgba(0, 115, 206, 0.1);
/* === GRADIENTS LIGHT === */
--gradient-primary: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%);
--gradient-card: linear-gradient(135deg, #ffffff 0%, #fcfcfd 100%);
--gradient-modal: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
--gradient-button: linear-gradient(135deg, var(--mb-primary) 0%, var(--mb-primary-dark) 100%);
}
/* ===== DARK MODE OVERRIDES ===== */
.dark {
/* === DARK MODE FOUNDATION === */
--bg-primary: #000000;
--bg-secondary: #0a0a0a;
--bg-tertiary: #1a1a1a;
--bg-card: #111111;
--bg-surface: #0d0d0d;
--bg-overlay: rgba(0, 0, 0, 0.95);
--bg-modal: rgba(17, 17, 17, 0.98);
/* === TEXT COLORS DARK === */
--text-primary: #ffffff;
--text-secondary: #e2e8f0;
--text-tertiary: #cbd5e1;
--text-muted: #94a3b8;
--text-accent: var(--mb-white);
--text-on-primary: var(--mb-black);
--text-success: #10b981;
--text-warning: #f59e0b;
--text-error: #ef4444;
/* === BORDER COLORS DARK === */
--border-primary: #1f2937;
--border-secondary: #374151;
--border-focus: var(--mb-white);
--border-error: #ef4444;
--border-success: #10b981;
/* === SHADOWS DARK === */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.6);
--shadow-modal: 0 25px 50px -12px rgba(0, 0, 0, 0.7);
/* === GLASSMORPHISM DARK === */
--glass-bg: rgba(17, 17, 17, 0.8);
--glass-border: rgba(255, 255, 255, 0.1);
--glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
--glass-blur: blur(16px);
/* === INTERACTIVE STATES DARK === */
--hover-bg: rgba(255, 255, 255, 0.04);
--active-bg: rgba(255, 255, 255, 0.08);
--focus-ring: 0 0 0 3px rgba(255, 255, 255, 0.1);
/* === GRADIENTS DARK === */
--gradient-primary: linear-gradient(135deg, #000000 0%, #0a0a0a 100%);
--gradient-card: linear-gradient(135deg, #111111 0%, #0d0d0d 100%);
--gradient-modal: linear-gradient(135deg, #1a1a1a 0%, #111111 100%);
--gradient-button: linear-gradient(135deg, var(--mb-white) 0%, #f0f0f0 100%);
}
/* ===== UNIFIED COMPONENT STYLES ===== */
/* === BUTTONS === */
.btn {
@apply inline-flex items-center justify-center px-4 py-2 text-sm font-medium rounded-lg transition-all duration-200;
background: var(--bg-card);
color: var(--text-primary);
border: 1px solid var(--border-primary);
box-shadow: var(--shadow-sm);
}
.btn:hover {
background: var(--hover-bg);
box-shadow: var(--shadow-md);
transform: translateY(-1px);
}
.btn:focus {
outline: none;
box-shadow: var(--focus-ring);
}
.btn-primary {
background: var(--gradient-button);
color: var(--text-on-primary);
border: none;
}
.btn-primary:hover {
background: var(--mb-primary-dark);
transform: translateY(-1px);
}
.dark .btn-primary {
background: var(--gradient-button);
color: var(--text-on-primary);
}
.dark .btn-primary:hover {
background: #f0f0f0;
}
/* === CARDS === */
.card {
background: var(--gradient-card);
border: 1px solid var(--border-primary);
border-radius: 16px;
box-shadow: var(--shadow-lg);
backdrop-filter: var(--glass-blur);
transition: all 0.3s ease;
}
.card:hover {
box-shadow: var(--shadow-xl);
transform: translateY(-2px);
}
.glass-card {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 16px;
box-shadow: var(--glass-shadow);
backdrop-filter: var(--glass-blur);
}
/* === MODALS === */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(8px);
z-index: 50;
}
.dark .modal-overlay {
background: rgba(0, 0, 0, 0.7);
}
.modal {
background: var(--gradient-modal);
border: 1px solid var(--border-primary);
border-radius: 20px;
box-shadow: var(--shadow-modal);
backdrop-filter: var(--glass-blur);
}
.modal-header {
border-bottom: 1px solid var(--border-primary);
color: var(--text-primary);
}
.modal-body {
color: var(--text-secondary);
}
/* === FORM ELEMENTS === */
.form-input {
@apply w-full px-4 py-3 rounded-lg transition-all duration-200;
background: var(--bg-card);
border: 1px solid var(--border-primary);
color: var(--text-primary);
}
.form-input:focus {
outline: none;
border-color: var(--border-focus);
box-shadow: var(--focus-ring);
}
.form-input::placeholder {
color: var(--text-muted);
}
.form-label {
@apply block text-sm font-medium mb-2;
color: var(--text-secondary);
}
/* === NAVIGATION === */
.nav-item {
@apply px-4 py-2 rounded-lg transition-all duration-200;
color: var(--text-secondary);
}
.nav-item:hover {
background: var(--hover-bg);
color: var(--text-primary);
}
.nav-item.active {
background: var(--active-bg);
color: var(--text-accent);
}
/* === ALERTS === */
.alert {
@apply p-4 rounded-lg border;
background: var(--bg-card);
border-color: var(--border-primary);
}
.alert-info {
background: rgba(0, 115, 206, 0.1);
border-color: var(--mb-primary);
color: var(--mb-primary);
}
.dark .alert-info {
background: rgba(0, 115, 206, 0.2);
color: var(--mb-white);
}
.alert-success {
background: rgba(5, 150, 105, 0.1);
border-color: var(--text-success);
color: var(--text-success);
}
.alert-warning {
background: rgba(217, 119, 6, 0.1);
border-color: var(--text-warning);
color: var(--text-warning);
}
.alert-error {
background: rgba(220, 38, 38, 0.1);
border-color: var(--text-error);
color: var(--text-error);
}
/* === TABLES === */
.table {
@apply w-full border-collapse;
}
.table th {
@apply px-6 py-4 text-left text-sm font-medium uppercase tracking-wider;
background: var(--bg-tertiary);
color: var(--text-tertiary);
border-bottom: 1px solid var(--border-primary);
}
.table td {
@apply px-6 py-4 text-sm;
color: var(--text-secondary);
border-bottom: 1px solid var(--border-primary);
}
.table tbody tr:hover {
background: var(--hover-bg);
}
/* === TOOLTIPS === */
.tooltip {
@apply absolute z-50 px-3 py-2 text-sm rounded-lg shadow-lg;
background: var(--bg-modal);
color: var(--text-primary);
border: 1px solid var(--border-primary);
backdrop-filter: var(--glass-blur);
}
/* === BADGES & STATUS === */
.badge {
@apply inline-flex items-center px-3 py-1 rounded-full text-xs font-medium;
background: var(--bg-tertiary);
color: var(--text-secondary);
}
.badge-primary {
background: var(--mb-primary);
color: var(--mb-white);
}
.badge-success {
background: var(--text-success);
color: var(--mb-white);
}
.badge-warning {
background: var(--text-warning);
color: var(--mb-white);
}
.badge-error {
background: var(--text-error);
color: var(--mb-white);
}
/* === LOADING STATES === */
.loading {
@apply animate-pulse;
background: var(--bg-tertiary);
}
.spinner {
@apply animate-spin rounded-full;
border: 2px solid var(--border-primary);
border-top-color: var(--text-accent);
}
/* === RESPONSIVE UTILITIES === */
@media (max-width: 768px) {
.modal {
margin: 1rem;
max-width: calc(100% - 2rem);
}
.card {
border-radius: 12px;
}
.btn {
@apply px-3 py-2 text-xs;
}
}
/* === ACCESSIBILITY IMPROVEMENTS === */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* === HIGH CONTRAST MODE === */
@media (prefers-contrast: high) {
:root {
--border-primary: #000000;
--text-primary: #000000;
}
.dark {
--border-primary: #ffffff;
--text-primary: #ffffff;
}
}
/* === PRINT STYLES === */
@media print {
.modal-overlay,
.tooltip,
.btn {
display: none !important;
}
.card {
box-shadow: none;
border: 1px solid #000000;
}
}
/* === LEGACY COMPATIBILITY === */
.mercedes-modal {
@extend .modal;
}
.glass-modal {
@extend .modal;
@extend .glass-card;
}
.mb-dashboard-card {
@extend .card;
}
.btn-professional {
@extend .btn-primary;
}
.input-professional {
@extend .form-input;
}
/* === CSS CUSTOM PROPERTIES FALLBACKS === */
.no-css-vars .btn-primary {
background: #0073ce;
color: #ffffff;
}
.no-css-vars .dark .btn-primary {
background: #ffffff;
color: #000000;
}
/* === THEME TRANSITION === */
* {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 200ms;
}
/* === UTILITY CLASSES === */
.theme-transition {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.focus-visible:focus {
outline: 2px solid var(--border-focus);
outline-offset: 2px;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}

File diff suppressed because one or more lines are too long

View File

@ -339,6 +339,24 @@ function createRequestRow(request) {
</button>
` : ''}
${request.status === 'approved' ? `
<button onclick="showOtpCode(${request.id})"
class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300 transition-colors"
title="OTP-Code anzeigen">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-3a1 1 0 011-1h2.586l6.243-6.243A6 6 0 0121 9z"/>
</svg>
</button>
<button onclick="revokeRequest(${request.id})"
class="text-orange-600 hover:text-orange-900 dark:text-orange-400 dark:hover:text-orange-300 transition-colors"
title="Genehmigung widerrufen">
<svg class="w-5 h-5" 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>
</button>
` : ''}
<button onclick="deleteRequest(${request.id})"
class="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300 transition-colors"
title="Löschen">
@ -871,17 +889,262 @@ function getTimeAgo(dateString) {
const now = new Date();
const date = new Date(dateString);
const diffMs = now - date;
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffHours / 24);
const diffInSeconds = Math.floor((now - date) / 1000);
if (diffDays > 0) {
return `vor ${diffDays} Tag${diffDays === 1 ? '' : 'en'}`;
} else if (diffHours > 0) {
return `vor ${diffHours} Stunde${diffHours === 1 ? '' : 'n'}`;
if (diffInSeconds < 60) return 'Gerade eben';
if (diffInSeconds < 3600) return `vor ${Math.floor(diffInSeconds / 60)} Min.`;
if (diffInSeconds < 86400) return `vor ${Math.floor(diffInSeconds / 3600)} Std.`;
if (diffInSeconds < 2592000) return `vor ${Math.floor(diffInSeconds / 86400)} Tagen`;
return formatDateTime(dateString);
}
/**
* OTP-Code-Verwaltung
*/
// OTP-Code für genehmigte Anfrage anzeigen
async function showOtpCode(requestId) {
try {
showLoading(true);
const url = `${API_BASE_URL}/api/admin/requests/${requestId}/otp`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (data.success && data.otp_code) {
showOtpModal(data.otp_code, data.request_id, data.expires_at, data.status);
} else {
showNotification('❌ ' + (data.error || 'OTP-Code konnte nicht abgerufen werden'), 'error');
}
} catch (error) {
console.error('Fehler beim Abrufen des OTP-Codes:', error);
showNotification('❌ Fehler beim Abrufen des OTP-Codes: ' + error.message, 'error');
} finally {
showLoading(false);
}
}
// OTP-Modal anzeigen
function showOtpModal(otpCode, requestId, expiresAt, status) {
// Bestehende Modals schließen
closeOtpModal();
const modal = document.createElement('div');
modal.className = 'fixed inset-0 bg-black/60 dark:bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4';
modal.id = 'otp-modal';
const expiryDate = expiresAt ? new Date(expiresAt).toLocaleString('de-DE') : 'Unbekannt';
const statusText = status === 'used' ? 'Bereits verwendet' :
status === 'expired' ? 'Abgelaufen' : 'Gültig';
const statusColor = status === 'used' ? 'text-red-600 dark:text-red-400' :
status === 'expired' ? 'text-orange-600 dark:text-orange-400' :
'text-green-600 dark:text-green-400';
modal.innerHTML = `
<div class="bg-white dark:bg-slate-900 rounded-2xl shadow-2xl border border-gray-200 dark:border-slate-700 max-w-md w-full transform transition-all duration-300 scale-95 opacity-0" id="otp-modal-content">
<div class="p-6 border-b border-gray-200 dark:border-slate-700">
<div class="flex justify-between items-center">
<h3 class="text-xl font-bold text-gray-900 dark:text-slate-100 flex items-center">
<svg class="w-6 h-6 mr-2 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-3a1 1 0 011-1h2.586l6.243-6.243A6 6 0 0121 9z"/>
</svg>
OTP-Code
</h3>
<button onclick="closeOtpModal()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
<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>
<div class="p-6">
<div class="text-center mb-6">
<div class="text-4xl font-mono font-bold text-slate-900 dark:text-slate-100 tracking-widest bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-slate-800 dark:to-slate-700 rounded-lg py-6 px-6 mb-4 border-2 border-dashed border-blue-200 dark:border-slate-600">
${otpCode}
</div>
<p class="text-sm text-slate-500 dark:text-slate-400">6-stelliger Zugangscode für den Gast</p>
</div>
<div class="space-y-3 text-sm bg-gray-50 dark:bg-slate-800 rounded-lg p-4">
<div class="flex justify-between items-center">
<span class="text-slate-600 dark:text-slate-400 font-medium">Status:</span>
<span class="${statusColor} font-semibold flex items-center">
<span class="w-2 h-2 rounded-full mr-2 ${status === 'used' ? 'bg-red-500' : status === 'expired' ? 'bg-orange-500' : 'bg-green-500'}"></span>
${statusText}
</span>
</div>
<div class="flex justify-between items-center">
<span class="text-slate-600 dark:text-slate-400 font-medium">Gültig bis:</span>
<span class="text-slate-900 dark:text-slate-100 font-mono text-xs">${expiryDate}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-slate-600 dark:text-slate-400 font-medium">Anfrage-ID:</span>
<span class="text-slate-900 dark:text-slate-100 font-mono">#${requestId}</span>
</div>
</div>
<div class="mt-6 flex space-x-3">
<button onclick="copyOtpCode('${otpCode}')"
class="flex-1 px-4 py-3 bg-blue-500 dark:bg-blue-600 text-white rounded-lg hover:bg-blue-600 dark:hover:bg-blue-700 transition-colors flex items-center justify-center font-medium">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
</svg>
Code kopieren
</button>
<button onclick="closeOtpModal()"
class="flex-1 px-4 py-3 bg-gray-500 dark:bg-gray-600 text-white rounded-lg hover:bg-gray-600 dark:hover:bg-gray-700 transition-colors flex items-center justify-center font-medium">
<svg class="w-5 h-5 mr-2" 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>
Schließen
</button>
</div>
<div class="mt-4 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
<p class="text-xs text-blue-700 dark:text-blue-300 flex items-start">
<svg class="w-4 h-4 mr-2 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span>Der Gast kann diesen Code auf der Startseite eingeben, um seinen genehmigten Druckauftrag zu starten.</span>
</p>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
// Animation einblenden
setTimeout(() => {
const content = document.getElementById('otp-modal-content');
if (content) {
content.classList.remove('scale-95', 'opacity-0');
content.classList.add('scale-100', 'opacity-100');
}
}, 10);
// Modal schließen bei Klick außerhalb
modal.addEventListener('click', function(e) {
if (e.target === modal) {
closeOtpModal();
}
});
// ESC-Taste zum Schließen
const escapeHandler = function(e) {
if (e.key === 'Escape') {
closeOtpModal();
document.removeEventListener('keydown', escapeHandler);
}
};
document.addEventListener('keydown', escapeHandler);
}
// OTP-Code in Zwischenablage kopieren
function copyOtpCode(code) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(code).then(() => {
showNotification('📋 OTP-Code in Zwischenablage kopiert', 'success');
}).catch(() => {
fallbackCopyTextToClipboard(code);
});
} else {
const diffMinutes = Math.floor(diffMs / (1000 * 60));
return `vor ${Math.max(1, diffMinutes)} Minute${diffMinutes === 1 ? '' : 'n'}`;
fallbackCopyTextToClipboard(code);
}
}
// Fallback für ältere Browser
function fallbackCopyTextToClipboard(text) {
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.top = "0";
textArea.style.left = "0";
textArea.style.position = "fixed";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand('copy');
if (successful) {
showNotification('📋 OTP-Code in Zwischenablage kopiert', 'success');
} else {
showNotification('❌ Fehler beim Kopieren des Codes', 'error');
}
} catch (err) {
showNotification('❌ Fehler beim Kopieren des Codes', 'error');
}
document.body.removeChild(textArea);
}
// OTP-Modal schließen
function closeOtpModal() {
const modal = document.getElementById('otp-modal');
if (modal) {
const content = document.getElementById('otp-modal-content');
if (content) {
content.classList.add('scale-95', 'opacity-0');
content.classList.remove('scale-100', 'opacity-100');
}
setTimeout(() => {
modal.remove();
}, 300);
}
}
// Genehmigung widerrufen
async function revokeRequest(requestId) {
if (!confirm('Möchten Sie die Genehmigung dieser Anfrage wirklich widerrufen? Der OTP-Code wird ungültig.')) {
return;
}
try {
showLoading(true);
const url = `${API_BASE_URL}/api/requests/${requestId}/deny`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({ reason: 'Genehmigung durch Administrator widerrufen' })
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (data.success) {
showNotification('✅ Genehmigung erfolgreich widerrufen', 'success');
loadGuestRequests(); // Tabelle aktualisieren
} else {
showNotification('❌ ' + (data.error || 'Fehler beim Widerrufen der Genehmigung'), 'error');
}
} catch (error) {
console.error('Fehler beim Widerrufen der Genehmigung:', error);
showNotification('❌ Fehler beim Widerrufen der Genehmigung: ' + error.message, 'error');
} finally {
showLoading(false);
}
}

View File

@ -1,23 +1,25 @@
/**
* Jobs Safety Fix - Mercedes-Benz MYP Platform
* Umfassende Lösung für "jobs undefined" Probleme
* Version: 1.0.0
* Version: 2.0.0 - Erweiterte Fehlerbehandlung
*/
(function() {
'use strict';
console.log('🛡️ Jobs Safety Fix wird geladen...');
console.log('🛡️ Jobs Safety Fix v2.0.0 wird geladen...');
/**
* Globale Jobs-Variable sicher initialisieren
*/
if (typeof window.jobsData === 'undefined') {
window.jobsData = [];
console.log('✅ window.jobsData initialisiert');
}
if (typeof window.filteredJobs === 'undefined') {
window.filteredJobs = [];
console.log('✅ window.filteredJobs initialisiert');
}
/**
@ -28,70 +30,122 @@
* Jobs sicher abrufen
*/
getJobs: function() {
if (window.jobManager && Array.isArray(window.jobManager.jobs)) {
return window.jobManager.jobs;
try {
if (window.jobManager && Array.isArray(window.jobManager.jobs)) {
return window.jobManager.jobs;
}
if (Array.isArray(window.jobsData)) {
return window.jobsData;
}
console.warn('⚠️ Keine gültigen Jobs gefunden, gebe leeres Array zurück');
return [];
} catch (error) {
console.error('❌ Fehler beim Abrufen der Jobs:', error);
return [];
}
if (Array.isArray(window.jobsData)) {
return window.jobsData;
}
return [];
},
/**
* Gefilterte Jobs sicher abrufen
*/
getFilteredJobs: function() {
if (Array.isArray(window.filteredJobs)) {
return window.filteredJobs;
try {
if (Array.isArray(window.filteredJobs)) {
return window.filteredJobs;
}
return this.getJobs();
} catch (error) {
console.error('❌ Fehler beim Abrufen der gefilterten Jobs:', error);
return [];
}
return this.getJobs();
},
/**
* Jobs sicher setzen
*/
setJobs: function(jobs) {
if (!Array.isArray(jobs)) {
console.warn('⚠️ Jobs ist kein Array, konvertiere zu leerem Array');
jobs = [];
try {
if (!Array.isArray(jobs)) {
console.warn('⚠️ Jobs ist kein Array, konvertiere zu leerem Array:', typeof jobs);
jobs = [];
}
if (window.jobManager) {
window.jobManager.jobs = jobs;
}
window.jobsData = jobs;
console.log(`${jobs.length} Jobs sicher gesetzt`);
// Event für andere Komponenten
window.dispatchEvent(new CustomEvent('jobsUpdated', {
detail: { jobs: jobs, count: jobs.length }
}));
} catch (error) {
console.error('❌ Fehler beim Setzen der Jobs:', error);
window.jobsData = [];
if (window.jobManager) {
window.jobManager.jobs = [];
}
}
if (window.jobManager) {
window.jobManager.jobs = jobs;
}
window.jobsData = jobs;
console.log(`${jobs.length} Jobs sicher gesetzt`);
},
/**
* Job sicher finden
*/
findJob: function(jobId) {
const jobs = this.getJobs();
return jobs.find(job => job && job.id && job.id.toString() === jobId.toString()) || null;
try {
const jobs = this.getJobs();
const job = jobs.find(job => job && job.id && job.id.toString() === jobId.toString());
if (!job) {
console.warn(`⚠️ Job mit ID ${jobId} nicht gefunden`);
}
return job || null;
} catch (error) {
console.error('❌ Fehler beim Suchen des Jobs:', error);
return null;
}
},
/**
* Jobs sicher filtern
*/
filterJobs: function(filterFn) {
const jobs = this.getJobs();
if (typeof filterFn !== 'function') {
return jobs;
}
try {
const jobs = this.getJobs();
if (typeof filterFn !== 'function') {
console.warn('⚠️ Filter-Funktion ist nicht gültig');
return jobs;
}
return jobs.filter(job => {
if (!job || typeof job !== 'object') {
return false;
}
return filterFn(job);
try {
return filterFn(job);
} catch (filterError) {
console.warn('⚠️ Fehler beim Filtern eines Jobs:', filterError);
return false;
}
});
} catch (error) {
console.error('❌ Fehler beim Filtern der Jobs:', error);
return jobs;
return this.getJobs();
}
},
/**
* Debug-Informationen ausgeben
*/
debugInfo: function() {
console.group('🔍 Jobs Debug Info');
console.log('jobsData:', window.jobsData);
console.log('filteredJobs:', window.filteredJobs);
console.log('jobManager:', window.jobManager);
console.log('jobManager.jobs:', window.jobManager?.jobs);
console.groupEnd();
}
};
@ -99,61 +153,72 @@
* JobManager Sicherheitsprüfungen
*/
function ensureJobManagerSafety() {
if (typeof window.jobManager !== 'undefined' && window.jobManager) {
// Sicherstellen, dass jobs ein Array ist
if (!Array.isArray(window.jobManager.jobs)) {
console.warn('⚠️ JobManager.jobs ist kein Array, korrigiere...');
window.jobManager.jobs = [];
}
// Originale loadJobs Methode wrappen
if (window.jobManager.loadJobs && typeof window.jobManager.loadJobs === 'function') {
const originalLoadJobs = window.jobManager.loadJobs;
window.jobManager.loadJobs = async function(...args) {
try {
await originalLoadJobs.apply(this, args);
// Nach dem Laden sicherstellen, dass jobs ein Array ist
if (!Array.isArray(this.jobs)) {
try {
if (typeof window.jobManager !== 'undefined' && window.jobManager) {
// Sicherstellen, dass jobs ein Array ist
if (!Array.isArray(window.jobManager.jobs)) {
console.warn('⚠️ JobManager.jobs ist kein Array, korrigiere...', typeof window.jobManager.jobs);
window.jobManager.jobs = [];
}
// Originale loadJobs Methode wrappen
if (window.jobManager.loadJobs && typeof window.jobManager.loadJobs === 'function' && !window.jobManager._loadJobsWrapped) {
const originalLoadJobs = window.jobManager.loadJobs;
window.jobManager.loadJobs = async function(...args) {
try {
console.log('🔄 Lade Jobs sicher...');
await originalLoadJobs.apply(this, args);
// Nach dem Laden sicherstellen, dass jobs ein Array ist
if (!Array.isArray(this.jobs)) {
console.warn('⚠️ Jobs nach dem Laden kein Array, korrigiere...');
this.jobs = [];
}
console.log(`${this.jobs.length} Jobs erfolgreich geladen`);
} catch (error) {
console.error('❌ Fehler beim sicheren Laden der Jobs:', error);
this.jobs = [];
throw error;
}
} catch (error) {
console.error('❌ Fehler beim sicheren Laden der Jobs:', error);
this.jobs = [];
throw error;
}
};
}
// Originale renderJobs Methode wrappen
if (window.jobManager.renderJobs && typeof window.jobManager.renderJobs === 'function') {
const originalRenderJobs = window.jobManager.renderJobs;
window.jobManager.renderJobs = function(...args) {
try {
// Sicherstellen, dass jobs ein Array ist vor dem Rendern
if (!Array.isArray(this.jobs)) {
console.warn('⚠️ Jobs ist kein Array vor dem Rendern, korrigiere...');
this.jobs = [];
}
return originalRenderJobs.apply(this, args);
} catch (error) {
console.error('❌ Fehler beim sicheren Rendern der Jobs:', error);
// Fallback-Rendering
const jobsList = document.getElementById('jobs-list');
if (jobsList) {
jobsList.innerHTML = `
<div class="text-center py-12">
<div class="text-red-400 dark:text-red-600 text-6xl mb-4">⚠️</div>
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">Renderingfehler</h3>
<p class="text-gray-500 dark:text-gray-400 mb-4">Jobs konnten nicht angezeigt werden</p>
<button onclick="window.jobManager.loadJobs()" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
Erneut laden
</button>
</div>
`;
}
}
};
};
window.jobManager._loadJobsWrapped = true;
}
// Originale renderJobs Methode wrappen
if (window.jobManager.renderJobs && typeof window.jobManager.renderJobs === 'function' && !window.jobManager._renderJobsWrapped) {
const originalRenderJobs = window.jobManager.renderJobs;
window.jobManager.renderJobs = function(...args) {
try {
// Sicherstellen, dass jobs ein Array ist vor dem Rendern
if (!Array.isArray(this.jobs)) {
console.warn('⚠️ Jobs ist kein Array vor dem Rendern, korrigiere...');
this.jobs = [];
}
console.log(`🎨 Rendere ${this.jobs.length} Jobs...`);
return originalRenderJobs.apply(this, args);
} catch (error) {
console.error('❌ Fehler beim sicheren Rendern der Jobs:', error);
// Fallback-Rendering
const jobsList = document.getElementById('jobs-list');
if (jobsList) {
jobsList.innerHTML = `
<div class="text-center py-12">
<div class="text-red-400 dark:text-red-600 text-6xl mb-4">⚠️</div>
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">Renderingfehler</h3>
<p class="text-gray-500 dark:text-gray-400 mb-4">Jobs konnten nicht angezeigt werden</p>
<button onclick="window.safeJobsOperations.debugInfo(); window.jobManager?.loadJobs?.()"
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
Erneut laden
</button>
</div>
`;
}
}
};
window.jobManager._renderJobsWrapped = true;
}
}
} catch (error) {
console.error('❌ Fehler bei JobManager-Sicherheitsprüfung:', error);
}
}
@ -161,97 +226,114 @@
* Globale Variablen-Überwachung
*/
function setupGlobalVariableWatching() {
// jobsData überwachen
let _jobsData = [];
Object.defineProperty(window, 'jobsData', {
get: function() {
return _jobsData;
},
set: function(value) {
if (!Array.isArray(value)) {
console.warn('⚠️ Versuche jobsData mit Non-Array zu setzen:', value);
_jobsData = [];
} else {
_jobsData = value;
}
},
enumerable: true,
configurable: true
});
// filteredJobs überwachen
let _filteredJobs = [];
Object.defineProperty(window, 'filteredJobs', {
get: function() {
return _filteredJobs;
},
set: function(value) {
if (!Array.isArray(value)) {
console.warn('⚠️ Versuche filteredJobs mit Non-Array zu setzen:', value);
_filteredJobs = [];
} else {
_filteredJobs = value;
}
},
enumerable: true,
configurable: true
});
try {
// jobsData überwachen
let _jobsData = window.jobsData || [];
Object.defineProperty(window, 'jobsData', {
get: function() {
return _jobsData;
},
set: function(value) {
if (!Array.isArray(value)) {
console.warn('⚠️ Versuche jobsData mit Non-Array zu setzen:', typeof value, value);
_jobsData = [];
} else {
_jobsData = value;
console.log(`✅ jobsData gesetzt: ${value.length} Jobs`);
}
},
enumerable: true,
configurable: true
});
// filteredJobs überwachen
let _filteredJobs = window.filteredJobs || [];
Object.defineProperty(window, 'filteredJobs', {
get: function() {
return _filteredJobs;
},
set: function(value) {
if (!Array.isArray(value)) {
console.warn('⚠️ Versuche filteredJobs mit Non-Array zu setzen:', typeof value, value);
_filteredJobs = [];
} else {
_filteredJobs = value;
console.log(`✅ filteredJobs gesetzt: ${value.length} Jobs`);
}
},
enumerable: true,
configurable: true
});
console.log('✅ Globale Variablen-Überwachung eingerichtet');
} catch (error) {
console.error('❌ Fehler bei Variablen-Überwachung:', error);
}
}
/**
* API-Response Validator
*/
window.validateJobsResponse = function(data) {
if (!data || typeof data !== 'object') {
console.warn('⚠️ Ungültige API-Response:', data);
try {
if (!data || typeof data !== 'object') {
console.warn('⚠️ Ungültige API-Response:', data);
return { jobs: [], total: 0 };
}
let jobs = [];
// Verschiedene Response-Formate unterstützen
if (Array.isArray(data.jobs)) {
jobs = data.jobs;
} else if (Array.isArray(data.data)) {
jobs = data.data;
} else if (Array.isArray(data)) {
jobs = data;
} else if (data.success && Array.isArray(data.jobs)) {
jobs = data.jobs;
}
// Jobs validieren
const validJobs = jobs.filter(job => {
if (!job || typeof job !== 'object') {
console.warn('⚠️ Ungültiges Job-Objekt gefiltert:', job);
return false;
}
if (!job.id) {
console.warn('⚠️ Job ohne ID gefiltert:', job);
return false;
}
return true;
});
console.log(`${validJobs.length} von ${jobs.length} Jobs validiert`);
return {
jobs: validJobs,
total: validJobs.length,
current_page: data.current_page || 1,
total_pages: data.total_pages || 1
};
} catch (error) {
console.error('❌ Fehler bei API-Response-Validierung:', error);
return { jobs: [], total: 0 };
}
let jobs = [];
// Verschiedene Response-Formate unterstützen
if (Array.isArray(data.jobs)) {
jobs = data.jobs;
} else if (Array.isArray(data.data)) {
jobs = data.data;
} else if (Array.isArray(data)) {
jobs = data;
} else if (data.success && Array.isArray(data.jobs)) {
jobs = data.jobs;
}
// Jobs validieren
jobs = jobs.filter(job => {
if (!job || typeof job !== 'object') {
console.warn('⚠️ Ungültiges Job-Objekt gefiltert:', job);
return false;
}
if (!job.id) {
console.warn('⚠️ Job ohne ID gefiltert:', job);
return false;
}
return true;
});
console.log(`${jobs.length} Jobs validiert`);
return {
jobs: jobs,
total: jobs.length,
current_page: data.current_page || 1,
total_pages: data.total_pages || 1
};
};
/**
* Fehlerbehandlung für undefined Jobs
*/
window.addEventListener('error', function(e) {
if (e.message && e.message.includes('jobs') && e.message.includes('undefined')) {
if (e.message && e.message.toLowerCase().includes('jobs') && e.message.toLowerCase().includes('undefined')) {
console.log('🛡️ Jobs undefined Fehler abgefangen:', e.message);
console.log('🛡️ Stack:', e.error?.stack);
// Versuche Jobs zu reparieren
window.safeJobsOperations.setJobs([]);
// Debug-Info ausgeben
window.safeJobsOperations.debugInfo();
e.preventDefault();
return false;
}
@ -261,7 +343,7 @@
* Promise rejection handler für Jobs
*/
window.addEventListener('unhandledrejection', function(e) {
if (e.reason && e.reason.message && e.reason.message.includes('jobs')) {
if (e.reason && e.reason.message && e.reason.message.toLowerCase().includes('jobs')) {
console.log('🛡️ Jobs Promise rejection abgefangen:', e.reason);
// Versuche Jobs zu reparieren
@ -274,34 +356,49 @@
/**
* DOM bereit - Setup starten
*/
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
function initializeSafety() {
try {
setupGlobalVariableWatching();
ensureJobManagerSafety();
});
console.log('✅ Jobs Safety Fix erfolgreich initialisiert');
} catch (error) {
console.error('❌ Fehler bei Safety Fix Initialisierung:', error);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeSafety);
} else {
setupGlobalVariableWatching();
ensureJobManagerSafety();
initializeSafety();
}
/**
* Periodische Sicherheitsprüfung
*/
setInterval(function() {
ensureJobManagerSafety();
// Prüfe ob globale Jobs-Variablen noch Arrays sind
if (!Array.isArray(window.jobsData)) {
console.warn('⚠️ jobsData ist kein Array mehr, repariere...');
window.jobsData = [];
try {
ensureJobManagerSafety();
// Prüfe ob globale Jobs-Variablen noch Arrays sind
if (!Array.isArray(window.jobsData)) {
console.warn('⚠️ jobsData ist kein Array mehr, repariere...', typeof window.jobsData);
window.jobsData = [];
}
if (!Array.isArray(window.filteredJobs)) {
console.warn('⚠️ filteredJobs ist kein Array mehr, repariere...', typeof window.filteredJobs);
window.filteredJobs = [];
}
} catch (error) {
console.error('❌ Fehler bei periodischer Sicherheitsprüfung:', error);
}
if (!Array.isArray(window.filteredJobs)) {
console.warn('⚠️ filteredJobs ist kein Array mehr, repariere...');
window.filteredJobs = [];
}
}, 10000); // Alle 10 Sekunden
}, 15000); // Alle 15 Sekunden
console.log('✅ Jobs Safety Fix erfolgreich geladen');
// Globale Debug-Funktion verfügbar machen
window.debugJobs = function() {
window.safeJobsOperations.debugInfo();
};
console.log('✅ Jobs Safety Fix v2.0.0 erfolgreich geladen');
})();

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,11 @@
// Backup Service Worker for MYP Platform
/**
* Backup Service Worker für MYP Platform
* Vereinfachte Offline-Funktionalität als Fallback
*/
const CACHE_NAME = 'myp-platform-backup-v1';
// Assets to cache
// Assets die gecacht werden sollen
const ASSETS_TO_CACHE = [
'/',
'/dashboard',
@ -12,7 +16,7 @@ const ASSETS_TO_CACHE = [
'/static/favicon.ico'
];
// Install event - cache core assets
// Install Event - Cache core assets
self.addEventListener('install', (event) => {
console.log('Backup SW: Installing...');
@ -26,10 +30,13 @@ self.addEventListener('install', (event) => {
console.log('Backup SW: Assets cached');
return self.skipWaiting();
})
.catch((error) => {
console.error('Backup SW: Error caching assets', error);
})
);
});
// Activate event - clean up old caches
// Activate Event - Clean up old caches
self.addEventListener('activate', (event) => {
console.log('Backup SW: Activating...');
@ -52,31 +59,56 @@ self.addEventListener('activate', (event) => {
);
});
// Fetch event - cache first, network fallback
// Fetch Event - Cache first, network fallback mit korrekter Response-Behandlung
self.addEventListener('fetch', (event) => {
// Nur GET-Requests und HTTP/HTTPS unterstützen
if (event.request.method !== 'GET' ||
(!event.request.url.startsWith('http:') && !event.request.url.startsWith('https:'))) {
return;
}
event.respondWith(
caches.match(event.request)
.then((response) => {
if (response) {
return response;
}
return fetch(event.request)
.then((fetchResponse) => {
// Don't cache non-success responses
if (!fetchResponse || fetchResponse.status !== 200 || fetchResponse.type !== 'basic') {
return fetchResponse;
}
// Cache successful responses
const responseToCache = fetchResponse.clone();
caches.open(CACHE_NAME)
.then((cache) => {
cache.put(event.request, responseToCache);
});
return fetchResponse;
});
})
handleRequest(event.request)
);
});
});
// Request-Handler mit korrekter Response-Behandlung
async function handleRequest(request) {
try {
// Zuerst im Cache suchen
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// Netzwerk-Request versuchen
const networkResponse = await fetch(request);
// Nur erfolgreiche Responses cachen
if (networkResponse && networkResponse.ok && networkResponse.type === 'basic') {
const cache = await caches.open(CACHE_NAME);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
console.error('Backup SW: Request failed', error);
// Cache-Fallback versuchen
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// Offline-Fallback Response
return new Response('Offline - Service nicht verfügbar', {
status: 503,
statusText: 'Service Unavailable',
headers: {
'Content-Type': 'text/plain; charset=utf-8'
}
});
}
}
console.log('Backup Service Worker: Script loaded successfully');

View File

@ -0,0 +1,330 @@
/**
* MYP Platform - Service Worker Debug & Test Utility
* Diagnose und Test-Tool für Service Worker Probleme
*/
class ServiceWorkerDebugger {
constructor() {
this.isDebugging = false;
this.testResults = [];
this.init();
}
init() {
console.log('[SW-Debug] Service Worker Debugger initialisiert');
// Automatische Diagnose beim Laden
if ('serviceWorker' in navigator) {
this.runDiagnostics();
} else {
console.error('[SW-Debug] Service Worker werden nicht unterstützt');
}
}
async runDiagnostics() {
console.log('[SW-Debug] Starte Service Worker Diagnose...');
try {
// 1. Service Worker Status prüfen
await this.checkServiceWorkerStatus();
// 2. Registrierte Service Worker auflisten
await this.listRegisteredServiceWorkers();
// 3. Cache-Status prüfen
await this.checkCacheStatus();
// 4. Test-Requests durchführen
await this.runTestRequests();
// 5. Ergebnisse ausgeben
this.outputResults();
} catch (error) {
console.error('[SW-Debug] Diagnose fehlgeschlagen:', error);
}
}
async checkServiceWorkerStatus() {
console.log('[SW-Debug] Prüfe Service Worker Status...');
try {
const registration = await navigator.serviceWorker.getRegistration();
if (registration) {
console.log('[SW-Debug] ✅ Service Worker registriert:', registration.scope);
if (registration.active) {
console.log('[SW-Debug] ✅ Service Worker aktiv:', registration.active.scriptURL);
this.testResults.push({
test: 'Service Worker Status',
status: 'PASS',
details: `Aktiv: ${registration.active.scriptURL}`
});
} else {
console.warn('[SW-Debug] ⚠️ Service Worker nicht aktiv');
this.testResults.push({
test: 'Service Worker Status',
status: 'WARN',
details: 'Service Worker registriert aber nicht aktiv'
});
}
if (registration.waiting) {
console.log('[SW-Debug] 🔄 Service Worker wartet auf Aktivierung');
}
if (registration.installing) {
console.log('[SW-Debug] 🔄 Service Worker wird installiert');
}
} else {
console.error('[SW-Debug] ❌ Kein Service Worker registriert');
this.testResults.push({
test: 'Service Worker Status',
status: 'FAIL',
details: 'Kein Service Worker registriert'
});
}
} catch (error) {
console.error('[SW-Debug] Fehler beim Prüfen des Service Worker Status:', error);
this.testResults.push({
test: 'Service Worker Status',
status: 'ERROR',
details: error.message
});
}
}
async listRegisteredServiceWorkers() {
console.log('[SW-Debug] Liste registrierte Service Worker...');
try {
const registrations = await navigator.serviceWorker.getRegistrations();
console.log(`[SW-Debug] ${registrations.length} Service Worker gefunden:`);
registrations.forEach((registration, index) => {
console.log(`[SW-Debug] ${index + 1}. Scope: ${registration.scope}`);
if (registration.active) {
console.log(`[SW-Debug] Aktiv: ${registration.active.scriptURL}`);
}
if (registration.waiting) {
console.log(`[SW-Debug] Wartend: ${registration.waiting.scriptURL}`);
}
if (registration.installing) {
console.log(`[SW-Debug] Installierend: ${registration.installing.scriptURL}`);
}
});
this.testResults.push({
test: 'Service Worker Registrierungen',
status: registrations.length > 0 ? 'PASS' : 'WARN',
details: `${registrations.length} Service Worker registriert`
});
} catch (error) {
console.error('[SW-Debug] Fehler beim Auflisten der Service Worker:', error);
this.testResults.push({
test: 'Service Worker Registrierungen',
status: 'ERROR',
details: error.message
});
}
}
async checkCacheStatus() {
console.log('[SW-Debug] Prüfe Cache Status...');
try {
const cacheNames = await caches.keys();
console.log(`[SW-Debug] ${cacheNames.length} Caches gefunden:`);
let totalEntries = 0;
for (const cacheName of cacheNames) {
const cache = await caches.open(cacheName);
const keys = await cache.keys();
console.log(`[SW-Debug] - ${cacheName}: ${keys.length} Einträge`);
totalEntries += keys.length;
}
this.testResults.push({
test: 'Cache Status',
status: 'PASS',
details: `${cacheNames.length} Caches mit ${totalEntries} Einträgen`
});
} catch (error) {
console.error('[SW-Debug] Fehler beim Prüfen des Cache Status:', error);
this.testResults.push({
test: 'Cache Status',
status: 'ERROR',
details: error.message
});
}
}
async runTestRequests() {
console.log('[SW-Debug] Führe Test-Requests durch...');
const testUrls = [
'/static/css/tailwind.min.css',
'/static/js/ui-components.js',
'/api/dashboard',
'/dashboard'
];
for (const url of testUrls) {
try {
console.log(`[SW-Debug] Teste Request: ${url}`);
const startTime = performance.now();
const response = await fetch(url);
const endTime = performance.now();
const responseTime = Math.round(endTime - startTime);
const servedBy = response.headers.get('X-Served-By') || 'Network';
if (response.ok) {
console.log(`[SW-Debug] ✅ ${url} - ${response.status} (${responseTime}ms, ${servedBy})`);
this.testResults.push({
test: `Request Test: ${url}`,
status: 'PASS',
details: `${response.status} - ${responseTime}ms - ${servedBy}`
});
} else {
console.warn(`[SW-Debug] ⚠️ ${url} - ${response.status} ${response.statusText}`);
this.testResults.push({
test: `Request Test: ${url}`,
status: 'WARN',
details: `${response.status} ${response.statusText}`
});
}
} catch (error) {
console.error(`[SW-Debug] ❌ ${url} - Fehler:`, error);
this.testResults.push({
test: `Request Test: ${url}`,
status: 'ERROR',
details: error.message
});
}
}
}
outputResults() {
console.log('[SW-Debug] === DIAGNOSE-ERGEBNISSE ===');
const passed = this.testResults.filter(r => r.status === 'PASS').length;
const warned = this.testResults.filter(r => r.status === 'WARN').length;
const failed = this.testResults.filter(r => r.status === 'FAIL').length;
const errors = this.testResults.filter(r => r.status === 'ERROR').length;
console.log(`[SW-Debug] Gesamt: ${this.testResults.length} Tests`);
console.log(`[SW-Debug] ✅ Erfolgreich: ${passed}`);
console.log(`[SW-Debug] ⚠️ Warnungen: ${warned}`);
console.log(`[SW-Debug] ❌ Fehlgeschlagen: ${failed}`);
console.log(`[SW-Debug] 💥 Fehler: ${errors}`);
// Detaillierte Ergebnisse
this.testResults.forEach(result => {
const icon = this.getStatusIcon(result.status);
console.log(`[SW-Debug] ${icon} ${result.test}: ${result.details}`);
});
// Empfehlungen ausgeben
this.outputRecommendations();
}
getStatusIcon(status) {
switch (status) {
case 'PASS': return '✅';
case 'WARN': return '⚠️';
case 'FAIL': return '❌';
case 'ERROR': return '💥';
default: return '❓';
}
}
outputRecommendations() {
console.log('[SW-Debug] === EMPFEHLUNGEN ===');
const hasErrors = this.testResults.some(r => r.status === 'ERROR' || r.status === 'FAIL');
if (hasErrors) {
console.log('[SW-Debug] 🔧 Probleme erkannt:');
console.log('[SW-Debug] - Service Worker Cache leeren: await caches.keys().then(names => Promise.all(names.map(name => caches.delete(name))))');
console.log('[SW-Debug] - Service Worker neu registrieren: navigator.serviceWorker.getRegistrations().then(regs => regs.forEach(reg => reg.unregister()))');
console.log('[SW-Debug] - Browser-Cache leeren und Seite neu laden');
} else {
console.log('[SW-Debug] ✅ Service Worker funktioniert korrekt');
}
}
// Utility-Methoden für manuelle Tests
async clearAllCaches() {
console.log('[SW-Debug] Lösche alle Caches...');
try {
const cacheNames = await caches.keys();
await Promise.all(cacheNames.map(name => caches.delete(name)));
console.log('[SW-Debug] ✅ Alle Caches gelöscht');
} catch (error) {
console.error('[SW-Debug] Fehler beim Löschen der Caches:', error);
}
}
async unregisterAllServiceWorkers() {
console.log('[SW-Debug] Deregistriere alle Service Worker...');
try {
const registrations = await navigator.serviceWorker.getRegistrations();
await Promise.all(registrations.map(reg => reg.unregister()));
console.log('[SW-Debug] ✅ Alle Service Worker deregistriert');
} catch (error) {
console.error('[SW-Debug] Fehler beim Deregistrieren der Service Worker:', error);
}
}
async forceServiceWorkerUpdate() {
console.log('[SW-Debug] Erzwinge Service Worker Update...');
try {
const registration = await navigator.serviceWorker.getRegistration();
if (registration) {
await registration.update();
console.log('[SW-Debug] ✅ Service Worker Update angefordert');
} else {
console.warn('[SW-Debug] Kein Service Worker zum Aktualisieren gefunden');
}
} catch (error) {
console.error('[SW-Debug] Fehler beim Service Worker Update:', error);
}
}
// Event-Listener für Service Worker Events
setupEventListeners() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('controllerchange', () => {
console.log('[SW-Debug] 🔄 Service Worker Controller hat gewechselt');
});
navigator.serviceWorker.addEventListener('message', (event) => {
console.log('[SW-Debug] 📨 Message vom Service Worker:', event.data);
});
}
}
}
// Globale Instanz für Debugging
window.swDebugger = new ServiceWorkerDebugger();
// Utility-Funktionen für die Konsole
window.swDebug = {
run: () => window.swDebugger.runDiagnostics(),
clearCaches: () => window.swDebugger.clearAllCaches(),
unregisterAll: () => window.swDebugger.unregisterAllServiceWorkers(),
forceUpdate: () => window.swDebugger.forceServiceWorkerUpdate()
};
console.log('[SW-Debug] Service Worker Debugger geladen. Verwende swDebug.run() für Diagnose.');

View File

@ -1,13 +1,14 @@
/**
* MYP Platform Service Worker
* Offline-First Caching Strategy
* Offline-First Caching Strategy für Mercedes-Benz 3D-Druck-Management
*/
// MYP Platform Service Worker
// Cache-Namen für verschiedene Ressourcen-Typen
const CACHE_NAME = 'myp-platform-cache-v1';
const STATIC_CACHE = 'myp-static-v1';
const DYNAMIC_CACHE = 'myp-dynamic-v1';
// Statische Assets die gecacht werden sollen
const ASSETS_TO_CACHE = [
'/',
'/dashboard',
@ -20,7 +21,7 @@ const ASSETS_TO_CACHE = [
'/static/favicon.ico'
];
// Static files patterns
// Patterns für statische Dateien
const STATIC_PATTERNS = [
/\.css$/,
/\.js$/,
@ -30,16 +31,21 @@ const STATIC_PATTERNS = [
/\.woff2?$/
];
// API request patterns to avoid caching
// API-Request-Patterns die nicht gecacht werden sollen
const API_PATTERNS = [
/^\/api\//,
/^\/auth\//,
/^\/api\/jobs/,
/^\/auth\//
];
// API-Endpoints die gecacht werden können
const API_CACHE_PATTERNS = [
/^\/api\/dashboard/,
/^\/api\/printers/,
/^\/api\/jobs/,
/^\/api\/stats/
];
// Install event - cache core assets
// Install Event - Cache core assets
self.addEventListener('install', (event) => {
console.log('Service Worker: Installing...');
@ -59,7 +65,7 @@ self.addEventListener('install', (event) => {
);
});
// Activate event - clean up old caches
// Activate Event - Clean up old caches
self.addEventListener('activate', (event) => {
console.log('Service Worker: Activating...');
@ -82,351 +88,343 @@ self.addEventListener('activate', (event) => {
);
});
// Fetch event - handle requests
// Fetch Event - Handle requests mit korrekter Response-Behandlung
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// Unterstütze sowohl HTTP als auch HTTPS
// Nur GET-Requests und HTTP/HTTPS unterstützen
if (request.method !== 'GET' ||
(url.protocol !== 'http:' && url.protocol !== 'https:')) {
return; // Lasse andere Requests durch
}
// Chrome-Extension-URLs ignorieren
if (url.protocol === 'chrome-extension:') {
return;
}
// Simple network-first approach
event.respondWith(
fetch(request)
.then((response) => {
// Cache successful static responses
if (response && response.status === 200 && isStaticFile(url.pathname) &&
(url.protocol === 'http:' || url.protocol === 'https:')) {
const responseClone = response.clone();
caches.open(STATIC_CACHE).then((cache) => {
cache.put(request, responseClone);
}).catch(err => {
console.warn('Failed to cache response:', err);
});
}
return response;
})
.catch(() => {
// Fallback to cache if network fails
return caches.match(request);
})
);
// Request-Typ bestimmen und entsprechend behandeln
if (isStaticFile(url.pathname)) {
event.respondWith(handleStaticFile(request));
} else if (isAPIRequest(url.pathname)) {
event.respondWith(handleAPIRequest(request));
} else if (isPageRequest(request)) {
event.respondWith(handlePageRequest(request));
} else {
// Fallback für andere Requests
event.respondWith(handleGenericRequest(request));
}
});
// Check if request is for a static file
// Statische Dateien behandeln - Cache First Strategy
async function handleStaticFile(request) {
try {
// Zuerst im Cache suchen
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// Netzwerk-Request versuchen
const networkResponse = await fetch(request);
// Erfolgreiche Responses cachen
if (networkResponse && networkResponse.ok) {
const cache = await caches.open(STATIC_CACHE);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
console.error('Service Worker: Error handling static file', error);
// Cache-Fallback versuchen
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// Offline-Fallback Response
return new Response('Offline - Datei nicht verfügbar', {
status: 503,
statusText: 'Service Unavailable',
headers: {
'Content-Type': 'text/plain; charset=utf-8'
}
});
}
}
// API-Requests behandeln - Network First mit Cache-Fallback
async function handleAPIRequest(request) {
try {
// Netzwerk-Request versuchen
const networkResponse = await fetch(request);
if (networkResponse && networkResponse.ok) {
// GET-Requests für bestimmte Endpoints cachen
if (request.method === 'GET' && shouldCacheAPIResponse(request.url)) {
const cache = await caches.open(DYNAMIC_CACHE);
cache.put(request, networkResponse.clone());
}
return networkResponse;
}
throw new Error(`HTTP ${networkResponse.status}`);
} catch (error) {
console.log('Service Worker: Network failed for API request, trying cache');
// Cache-Fallback für GET-Requests
if (request.method === 'GET') {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
}
// Offline-Response für API-Requests
return new Response(JSON.stringify({
error: 'Offline - Daten nicht verfügbar',
offline: true,
timestamp: new Date().toISOString()
}), {
status: 503,
statusText: 'Service Unavailable',
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
});
}
}
// Seiten-Requests behandeln - Network First mit Offline-Fallback
async function handlePageRequest(request) {
try {
// Netzwerk-Request versuchen
const networkResponse = await fetch(request);
if (networkResponse && networkResponse.ok) {
// Erfolgreiche Seiten-Responses cachen
const cache = await caches.open(DYNAMIC_CACHE);
cache.put(request, networkResponse.clone());
return networkResponse;
}
throw new Error(`HTTP ${networkResponse.status}`);
} catch (error) {
console.log('Service Worker: Network failed for page request, trying cache');
// Cache-Fallback versuchen
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// Offline-Seite als Fallback
return new Response(`
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MYP Platform - Offline</title>
<style>
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
.offline-message { max-width: 500px; margin: 0 auto; }
</style>
</head>
<body>
<div class="offline-message">
<h1>Offline</h1>
<p>Sie sind momentan offline. Bitte überprüfen Sie Ihre Internetverbindung.</p>
<button onclick="window.location.reload()">Erneut versuchen</button>
</div>
</body>
</html>
`, {
status: 200,
statusText: 'OK',
headers: {
'Content-Type': 'text/html; charset=utf-8'
}
});
}
}
// Generische Request-Behandlung
async function handleGenericRequest(request) {
try {
return await fetch(request);
} catch (error) {
console.error('Service Worker: Generic request failed', error);
return new Response('Request failed', {
status: 503,
statusText: 'Service Unavailable',
headers: {
'Content-Type': 'text/plain; charset=utf-8'
}
});
}
}
// Hilfsfunktionen für Request-Typ-Erkennung
function isStaticFile(pathname) {
return STATIC_PATTERNS.some(pattern => pattern.test(pathname));
}
// Check if request is an API request
function isAPIRequest(pathname) {
return API_PATTERNS.some(pattern => pattern.test(pathname));
}
// Check if request is for a page
function isPageRequest(request) {
return request.mode === 'navigate';
return request.mode === 'navigate' ||
request.headers.get('accept')?.includes('text/html');
}
// MYP Platform Service Worker
const STATIC_FILES = [
'/',
'/static/css/tailwind.min.css',
'/static/css/tailwind-dark.min.css',
'/static/js/ui-components.js',
'/static/js/offline-app.js',
'/login',
'/dashboard'
];
// API endpoints to cache
const API_CACHE_PATTERNS = [
/^\/api\/dashboard/,
/^\/api\/printers/,
/^\/api\/jobs/,
/^\/api\/stats/
];
// Handle static file requests - Cache First strategy
async function handleStaticFile(request) {
try {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
const networkResponse = await fetch(request);
// Cache successful responses
if (networkResponse.ok) {
const cache = await caches.open(STATIC_CACHE);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
console.error('Service Worker: Error handling static file', error);
// Return cached version if available
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// Return offline fallback
return new Response('Offline - Datei nicht verfügbar', {
status: 503,
statusText: 'Service Unavailable'
});
}
function shouldCacheAPIResponse(url) {
const pathname = new URL(url).pathname;
return API_CACHE_PATTERNS.some(pattern => pattern.test(pathname));
}
// Handle API requests - Network First with cache fallback
async function handleAPIRequest(request) {
const url = new URL(request.url);
// Skip caching for chrome-extension URLs
if (url.protocol === 'chrome-extension:') {
try {
return await fetch(request);
} catch (error) {
console.error('Failed to fetch from chrome-extension:', error);
return new Response(JSON.stringify({
error: 'Fehler beim Zugriff auf chrome-extension',
offline: true
}), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
}
try {
// Try network first
const networkResponse = await fetch(request);
if (networkResponse.ok) {
// Cache successful GET responses for specific endpoints
if (request.method === 'GET' && shouldCacheAPIResponse(url.pathname)) {
const cache = await caches.open(DYNAMIC_CACHE);
cache.put(request, networkResponse.clone());
}
return networkResponse;
}
throw new Error(`HTTP ${networkResponse.status}`);
} catch (error) {
console.log('Service Worker: Network failed for API request, trying cache');
// Try cache fallback for GET requests
const cachedResponse = await caches.match(request);
if (cachedResponse) {
// Add offline header to indicate cached response
const response = cachedResponse.clone();
response.headers.set('X-Served-By', 'ServiceWorker-Cache');
return response;
}
// Return offline response
return new Response(JSON.stringify({
error: 'Offline - Daten nicht verfügbar',
offline: true,
timestamp: new Date().toISOString()
}), {
status: 503,
statusText: 'Service Unavailable',
headers: {
'Content-Type': 'application/json',
'X-Served-By': 'ServiceWorker-Offline'
}
});
}
}
// Handle page requests - Network First with offline fallback
async function handlePageRequest(request) {
try {
const networkResponse = await fetch(request);
if (networkResponse.ok) {
// Cache successful page responses
const cache = await caches.open(DYNAMIC_CACHE);
cache.put(request, networkResponse.clone());
return networkResponse;
}
throw new Error(`HTTP ${networkResponse.status}`);
} catch (error) {
console.log('Service Worker: Network failed for page request, trying cache');
// Try cache fallback
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// Fallback offline response
return caches.match('/offline.html') || new Response(
'<html><body><h1>Offline</h1><p>Sie sind momentan offline. Bitte überprüfen Sie Ihre Internetverbindung.</p></body></html>',
{
status: 200,
headers: { 'Content-Type': 'text/html' }
}
);
}
}
// Check if API response should be cached
function shouldCacheAPIResponse(pathname) {
return API_CACHE_PATTERNS.some(pattern => pattern.test(pathname));
}
// Background sync for offline actions
// Background Sync für Offline-Aktionen
self.addEventListener('sync', (event) => {
console.log('Service Worker: Background sync triggered', event.tag);
if (event.tag === 'background-sync') {
event.waitUntil(doBackgroundSync());
}
console.log('Service Worker: Background sync triggered', event.tag);
if (event.tag === 'background-sync') {
event.waitUntil(doBackgroundSync());
}
});
// Perform background sync
async function doBackgroundSync() {
try {
// Get pending requests from IndexedDB or localStorage
const pendingRequests = await getPendingRequests();
for (const request of pendingRequests) {
try {
await fetch(request.url, request.options);
await removePendingRequest(request.id);
console.log('Service Worker: Synced request', request.url);
} catch (error) {
console.error('Service Worker: Failed to sync request', request.url, error);
}
}
} catch (error) {
console.error('Service Worker: Background sync failed', error);
try {
// Pending Requests aus IndexedDB oder localStorage holen
const pendingRequests = await getPendingRequests();
for (const request of pendingRequests) {
try {
await fetch(request.url, request.options);
await removePendingRequest(request.id);
console.log('Service Worker: Synced request', request.url);
} catch (error) {
console.error('Service Worker: Failed to sync request', request.url, error);
}
}
} catch (error) {
console.error('Service Worker: Background sync failed', error);
}
}
// Get pending requests (placeholder - implement with IndexedDB)
// Placeholder-Funktionen für IndexedDB-Integration
async function getPendingRequests() {
// This would typically use IndexedDB to store pending requests
// For now, return empty array
return [];
return [];
}
// Remove pending request (placeholder - implement with IndexedDB)
async function removePendingRequest(id) {
// This would typically remove the request from IndexedDB
console.log('Service Worker: Removing pending request', id);
console.log('Service Worker: Removing pending request', id);
}
// Push notification handling
// Push Notification Handling
self.addEventListener('push', (event) => {
console.log('Service Worker: Push notification received');
const options = {
body: 'Sie haben neue Benachrichtigungen',
icon: '/static/icons/icon-192x192.png',
badge: '/static/icons/badge-72x72.png',
vibrate: [100, 50, 100],
data: {
dateOfArrival: Date.now(),
primaryKey: 1
},
actions: [
{
action: 'explore',
title: 'Anzeigen',
icon: '/static/icons/checkmark.png'
},
{
action: 'close',
title: 'Schließen',
icon: '/static/icons/xmark.png'
}
]
};
event.waitUntil(
self.registration.showNotification('MYP Platform', options)
);
console.log('Service Worker: Push notification received');
const options = {
body: 'Sie haben neue Benachrichtigungen',
icon: '/static/icons/icon-192x192.png',
badge: '/static/icons/badge-72x72.png',
vibrate: [100, 50, 100],
data: {
dateOfArrival: Date.now(),
primaryKey: 1
},
actions: [
{
action: 'explore',
title: 'Anzeigen',
icon: '/static/icons/checkmark.png'
},
{
action: 'close',
title: 'Schließen',
icon: '/static/icons/xmark.png'
}
]
};
event.waitUntil(
self.registration.showNotification('MYP Platform', options)
);
});
// Notification click handling
// Notification Click Handling
self.addEventListener('notificationclick', (event) => {
console.log('Service Worker: Notification clicked');
event.notification.close();
if (event.action === 'explore') {
event.waitUntil(
clients.openWindow('/dashboard')
);
}
console.log('Service Worker: Notification clicked');
event.notification.close();
if (event.action === 'explore') {
event.waitUntil(
clients.openWindow('/dashboard')
);
}
});
// Message handling from main thread
// Message Handling vom Main Thread
self.addEventListener('message', (event) => {
console.log('Service Worker: Message received', event.data);
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
if (event.data && event.data.type === 'CACHE_URLS') {
event.waitUntil(
cacheUrls(event.data.urls)
);
}
console.log('Service Worker: Message received', event.data);
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
if (event.data && event.data.type === 'CACHE_URLS') {
event.waitUntil(
cacheUrls(event.data.urls)
);
}
});
// Cache specific URLs
// Spezifische URLs cachen
async function cacheUrls(urls) {
try {
const cache = await caches.open(DYNAMIC_CACHE);
await cache.addAll(urls);
console.log('Service Worker: URLs cached', urls);
} catch (error) {
console.error('Service Worker: Error caching URLs', error);
}
try {
const cache = await caches.open(DYNAMIC_CACHE);
await cache.addAll(urls);
console.log('Service Worker: URLs cached', urls);
} catch (error) {
console.error('Service Worker: Error caching URLs', error);
}
}
// Periodic background sync (if supported)
// Periodic Background Sync (falls unterstützt)
self.addEventListener('periodicsync', (event) => {
console.log('Service Worker: Periodic sync triggered', event.tag);
if (event.tag === 'content-sync') {
event.waitUntil(syncContent());
}
console.log('Service Worker: Periodic sync triggered', event.tag);
if (event.tag === 'content-sync') {
event.waitUntil(syncContent());
}
});
// Sync content periodically
// Content periodisch synchronisieren
async function syncContent() {
try {
// Sync critical data in background
const endpoints = ['/api/dashboard', '/api/jobs'];
for (const endpoint of endpoints) {
try {
const response = await fetch(endpoint);
if (response.ok) {
const cache = await caches.open(DYNAMIC_CACHE);
cache.put(endpoint, response.clone());
}
} catch (error) {
console.error('Service Worker: Error syncing', endpoint, error);
}
try {
const endpoints = ['/api/dashboard', '/api/jobs'];
for (const endpoint of endpoints) {
try {
const response = await fetch(endpoint);
if (response && response.ok) {
const cache = await caches.open(DYNAMIC_CACHE);
cache.put(endpoint, response.clone());
}
} catch (error) {
console.error('Service Worker: Content sync failed', error);
} catch (error) {
console.error('Service Worker: Error syncing', endpoint, error);
}
}
} catch (error) {
console.error('Service Worker: Content sync failed', error);
}
}
console.log('Service Worker: Script loaded');
console.log('Service Worker: Script loaded successfully');

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,525 @@
/**
* Unified Theme Manager für MYP Platform
* =====================================
*
* Konsolidiert alle Dark/Light Mode JavaScript-Implementierungen
* Ersetzt: dark-mode.js, verschiedene inline Scripts
*
* Author: Till Tomczak - MYP Team
* Zweck: Einheitliches Theme-Management und Behebung von Inkonsistenzen
*/
class UnifiedThemeManager {
constructor() {
// Einheitlicher Storage Key (konsolidiert alle verschiedenen Keys)
this.storageKey = 'myp-dark-mode';
this.oldStorageKeys = ['darkMode', 'theme', 'dark-theme']; // Legacy Keys für Migration
// DOM Elemente
this.htmlElement = document.documentElement;
this.toggleButtons = [];
this.themeIndicators = [];
// Event Callbacks
this.callbacks = [];
// Debug Modus
this.debug = false;
// Initialisierung
this.init();
// Globale Instanz für Legacy-Kompatibilität
window.themeManager = this;
}
/**
* Initialisiert den Theme Manager
*/
init() {
this.log('🎨 Unified Theme Manager initialisiert');
// Legacy Storage Keys migrieren
this.migrateLegacyStorage();
// Theme aus Storage laden oder System-Präferenz verwenden
this.loadTheme();
// Event Listener registrieren
this.setupEventListeners();
// DOM-Elemente suchen und registrieren
this.findThemeElements();
// Initial UI aktualisieren
this.updateUI();
// CSS Custom Properties setzen
this.updateCSSProperties();
}
/**
* Migriert alte Storage Keys zum einheitlichen System
*/
migrateLegacyStorage() {
for (const oldKey of this.oldStorageKeys) {
const oldValue = localStorage.getItem(oldKey);
if (oldValue && !localStorage.getItem(this.storageKey)) {
// Normalisiere den Wert
const isDark = oldValue === 'true' || oldValue === 'dark';
localStorage.setItem(this.storageKey, isDark.toString());
localStorage.removeItem(oldKey); // Alten Key entfernen
this.log(`🔄 Migriert ${oldKey}: ${oldValue}${this.storageKey}: ${isDark}`);
}
}
}
/**
* Lädt das Theme aus Storage oder verwendet System-Präferenz
*/
loadTheme() {
let theme = localStorage.getItem(this.storageKey);
if (theme === null) {
// Keine gespeicherte Präferenz → System-Präferenz verwenden
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
theme = prefersDark ? 'true' : 'false';
this.log(`🌟 Keine gespeicherte Präferenz → System: ${prefersDark ? 'Dark' : 'Light'} Mode`);
}
const isDark = theme === 'true';
this.setTheme(isDark, false); // Ohne Storage-Update da bereits aus Storage geladen
}
/**
* Event Listener für System-Präferenz-Änderungen und Keyboard-Shortcuts
*/
setupEventListeners() {
// System-Präferenz-Änderungen
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', (e) => {
// Nur reagieren wenn keine manuelle Präferenz gesetzt wurde
const storedTheme = localStorage.getItem(this.storageKey);
if (storedTheme === null) {
this.setTheme(e.matches);
this.log(`🔄 System-Präferenz geändert: ${e.matches ? 'Dark' : 'Light'} Mode`);
}
});
// Keyboard Shortcut (Ctrl/Cmd + Shift + D)
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'D') {
e.preventDefault();
this.toggle();
this.log('⌨️ Theme per Keyboard-Shortcut umgeschaltet');
}
});
// Page Visibility API für Theme-Synchronisation zwischen Tabs
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
this.syncThemeFromStorage();
}
});
// Storage Event für Cross-Tab-Synchronisation
window.addEventListener('storage', (e) => {
if (e.key === this.storageKey && e.newValue !== null) {
const isDark = e.newValue === 'true';
this.setTheme(isDark, false); // Ohne Storage-Update
this.log(`🔄 Theme aus anderem Tab synchronisiert: ${isDark ? 'Dark' : 'Light'} Mode`);
}
});
// CSS-Transition Event für sanfte Übergänge
this.htmlElement.addEventListener('transitionend', this.handleTransitionEnd.bind(this));
}
/**
* Sucht und registriert alle Theme-bezogenen DOM-Elemente
*/
findThemeElements() {
// Toggle Buttons finden
const toggleSelectors = [
'[data-theme-toggle]',
'.theme-toggle',
'.dark-mode-toggle',
'#theme-toggle',
'#darkModeToggle'
];
this.toggleButtons = [];
toggleSelectors.forEach(selector => {
const elements = document.querySelectorAll(selector);
elements.forEach(element => {
this.registerToggleButton(element);
});
});
// Theme Indikatoren finden
const indicatorSelectors = [
'[data-theme-indicator]',
'.theme-indicator',
'.dark-mode-indicator'
];
this.themeIndicators = [];
indicatorSelectors.forEach(selector => {
const elements = document.querySelectorAll(selector);
elements.forEach(element => {
this.themeIndicators.push(element);
});
});
this.log(`🔍 Gefunden: ${this.toggleButtons.length} Toggle-Buttons, ${this.themeIndicators.length} Indikatoren`);
}
/**
* Registriert einen Toggle-Button
*/
registerToggleButton(element) {
if (this.toggleButtons.includes(element)) return;
element.addEventListener('click', (e) => {
e.preventDefault();
this.toggle();
});
// Accessibility
element.setAttribute('role', 'button');
element.setAttribute('aria-label', 'Theme umschalten');
this.toggleButtons.push(element);
}
/**
* Theme umschalten
*/
toggle() {
const currentTheme = this.isDarkMode();
const newTheme = !currentTheme;
this.setTheme(newTheme);
this.log(`🔄 Theme umgeschaltet: ${currentTheme ? 'Dark' : 'Light'}${newTheme ? 'Dark' : 'Light'} Mode`);
// Animation für visuelles Feedback
this.triggerToggleAnimation();
}
/**
* Theme setzen
*/
setTheme(isDark, updateStorage = true) {
const previousTheme = this.isDarkMode();
// HTML Klasse setzen/entfernen
this.htmlElement.classList.toggle('dark', isDark);
// Storage aktualisieren
if (updateStorage) {
localStorage.setItem(this.storageKey, isDark.toString());
}
// CSS Custom Properties aktualisieren
this.updateCSSProperties();
// UI-Elemente aktualisieren
this.updateUI();
// Meta Theme-Color aktualisieren
this.updateMetaThemeColor(isDark);
// Callbacks ausführen
this.executeCallbacks(isDark, previousTheme);
// Custom Event dispatchen
this.dispatchThemeEvent(isDark);
this.log(`🎨 Theme gesetzt: ${isDark ? 'Dark' : 'Light'} Mode`);
}
/**
* Aktueller Theme-Status
*/
isDarkMode() {
return this.htmlElement.classList.contains('dark');
}
/**
* Theme als String
*/
getTheme() {
return this.isDarkMode() ? 'dark' : 'light';
}
/**
* UI-Elemente aktualisieren
*/
updateUI() {
const isDark = this.isDarkMode();
const themeText = isDark ? 'Dark' : 'Light';
// Toggle Buttons aktualisieren
this.toggleButtons.forEach(button => {
// Icon aktualisieren
const iconElement = button.querySelector('.theme-icon') || button.querySelector('i') || button;
if (iconElement) {
// Entferne alle Theme-Icon-Klassen
iconElement.classList.remove('fa-sun', 'fa-moon', 'fa-lightbulb', 'fa-moon-o');
// Füge entsprechendes Icon hinzu
if (isDark) {
iconElement.classList.add('fa-sun');
iconElement.title = 'Zu Light Mode wechseln';
} else {
iconElement.classList.add('fa-moon');
iconElement.title = 'Zu Dark Mode wechseln';
}
}
// Text aktualisieren (falls vorhanden)
const textElement = button.querySelector('.theme-text');
if (textElement) {
textElement.textContent = isDark ? 'Light Mode' : 'Dark Mode';
}
// Aria-Label aktualisieren
button.setAttribute('aria-label', `Zu ${isDark ? 'Light' : 'Dark'} Mode wechseln`);
// Data-Attribute für CSS-Styling
button.setAttribute('data-theme', this.getTheme());
});
// Theme Indikatoren aktualisieren
this.themeIndicators.forEach(indicator => {
indicator.textContent = `${themeText} Mode`;
indicator.setAttribute('data-theme', this.getTheme());
});
// Body-Klassen für Legacy-Kompatibilität
document.body.classList.toggle('dark-mode', isDark);
document.body.classList.toggle('light-mode', !isDark);
}
/**
* CSS Custom Properties aktualisieren
*/
updateCSSProperties() {
const isDark = this.isDarkMode();
// Zusätzliche CSS Properties für JavaScript-basierte Komponenten
this.htmlElement.style.setProperty('--theme-mode', isDark ? 'dark' : 'light');
this.htmlElement.style.setProperty('--theme-multiplier', isDark ? '-1' : '1');
// Für Chart.js und andere Libraries
this.htmlElement.style.setProperty('--chart-text-color', isDark ? '#ffffff' : '#111827');
this.htmlElement.style.setProperty('--chart-grid-color', isDark ? '#374151' : '#e5e7eb');
}
/**
* Meta Theme-Color für Mobile Browser aktualisieren
*/
updateMetaThemeColor(isDark) {
let metaThemeColor = document.querySelector('meta[name="theme-color"]');
if (!metaThemeColor) {
metaThemeColor = document.createElement('meta');
metaThemeColor.name = 'theme-color';
document.head.appendChild(metaThemeColor);
}
metaThemeColor.content = isDark ? '#000000' : '#ffffff';
}
/**
* Theme-Synchronisation aus Storage
*/
syncThemeFromStorage() {
const storedTheme = localStorage.getItem(this.storageKey);
if (storedTheme !== null) {
const isDark = storedTheme === 'true';
if (this.isDarkMode() !== isDark) {
this.setTheme(isDark, false);
}
}
}
/**
* Toggle-Animation auslösen
*/
triggerToggleAnimation() {
// CSS-Klasse für Animation hinzufügen
this.htmlElement.classList.add('theme-transitioning');
// Animation nach kurzer Zeit entfernen
setTimeout(() => {
this.htmlElement.classList.remove('theme-transitioning');
}, 300);
}
/**
* Transition End Handler
*/
handleTransitionEnd(event) {
if (event.propertyName === 'background-color' && event.target === this.htmlElement) {
this.log('🎨 Theme-Transition abgeschlossen');
}
}
/**
* Custom Event dispatchen
*/
dispatchThemeEvent(isDark) {
const event = new CustomEvent('themeChanged', {
detail: {
theme: this.getTheme(),
isDark: isDark,
timestamp: Date.now()
}
});
document.dispatchEvent(event);
}
/**
* Callback registrieren
*/
onThemeChange(callback) {
if (typeof callback === 'function') {
this.callbacks.push(callback);
}
}
/**
* Callback entfernen
*/
offThemeChange(callback) {
const index = this.callbacks.indexOf(callback);
if (index > -1) {
this.callbacks.splice(index, 1);
}
}
/**
* Alle Callbacks ausführen
*/
executeCallbacks(isDark, previousTheme) {
this.callbacks.forEach(callback => {
try {
callback(isDark, previousTheme);
} catch (error) {
console.error('Theme Callback Fehler:', error);
}
});
}
/**
* Debug-Logging
*/
log(message) {
if (this.debug || localStorage.getItem('myp-theme-debug') === 'true') {
console.log(`[ThemeManager] ${message}`);
}
}
/**
* Debug-Modus aktivieren/deaktivieren
*/
setDebug(enabled) {
this.debug = enabled;
localStorage.setItem('myp-theme-debug', enabled.toString());
}
/**
* System-Präferenz abfragen
*/
getSystemPreference() {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
/**
* Theme auf System-Präferenz zurücksetzen
*/
resetToSystemPreference() {
localStorage.removeItem(this.storageKey);
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
this.setTheme(prefersDark, false);
this.log(`🔄 Theme auf System-Präferenz zurückgesetzt: ${prefersDark ? 'Dark' : 'Light'} Mode`);
}
/**
* Theme-Status für APIs
*/
getStatus() {
return {
currentTheme: this.getTheme(),
isDarkMode: this.isDarkMode(),
systemPreference: this.getSystemPreference(),
storageKey: this.storageKey,
toggleButtons: this.toggleButtons.length,
indicators: this.themeIndicators.length,
callbacks: this.callbacks.length
};
}
}
// ===== LEGACY COMPATIBILITY =====
// Globale Funktionen für Legacy-Kompatibilität
window.toggleDarkMode = function() {
if (window.themeManager) {
window.themeManager.toggle();
}
};
window.setDarkMode = function(enabled) {
if (window.themeManager) {
window.themeManager.setTheme(enabled);
}
};
window.isDarkMode = function() {
return window.themeManager ? window.themeManager.isDarkMode() : false;
};
// ===== AUTO-INITIALIZATION =====
// Theme Manager automatisch initialisieren wenn DOM bereit ist
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
new UnifiedThemeManager();
});
} else {
new UnifiedThemeManager();
}
// ===== CSS TRANSITION SUPPORT =====
// CSS für sanfte Theme-Übergänge hinzufügen
const style = document.createElement('style');
style.textContent = `
.theme-transitioning,
.theme-transitioning * {
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease !important;
}
.theme-toggle {
transition: transform 0.2s ease;
}
.theme-toggle:hover {
transform: scale(1.1);
}
.theme-toggle:active {
transform: scale(0.95);
}
`;
document.head.appendChild(style);
console.log('🎨 Unified Theme Manager geladen');
// Export für Module
if (typeof module !== 'undefined' && module.exports) {
module.exports = UnifiedThemeManager;
}