🔧 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:
482
backend/static/css/dark-light-unified.css
Normal file
482
backend/static/css/dark-light-unified.css
Normal 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;
|
||||
}
|
2
backend/static/css/tailwind.min.css
vendored
2
backend/static/css/tailwind.min.css
vendored
File diff suppressed because one or more lines are too long
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
@ -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');
|
330
backend/static/js/sw-debug.js
Normal file
330
backend/static/js/sw-debug.js
Normal 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.');
|
@ -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');
|
26
backend/static/js/sw.min.js
vendored
26
backend/static/js/sw.min.js
vendored
File diff suppressed because one or more lines are too long
525
backend/static/js/theme-manager-unified.js
Normal file
525
backend/static/js/theme-manager-unified.js
Normal 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;
|
||||
}
|
Reference in New Issue
Block a user