📚 Improved error handling documentation and codebase organization in the frontend application. 🖥️🔍

This commit is contained in:
Till Tomczak 2025-05-30 20:47:28 +02:00
parent d1a6281577
commit 0d966712a7
5 changed files with 871 additions and 19 deletions

View File

@ -1,3 +1,98 @@
## ✅ 30.05.2025 21:15 - SQLite WAL-Dateien beim Programmende bereinigen
### Problem
Nach dem Beenden des Programms blieben zwei SQLite-Datenbankdateien zurück:
- `myp.db-shm` (Shared Memory)
- `myp.db-wal` (Write-Ahead Log)
Diese Dateien sind Teil des SQLite WAL-Modus (Write-Ahead Logging) und sollten normalerweise beim ordnungsgemäßen Herunterfahren automatisch aufgeräumt werden.
### Root-Cause-Analyse
**SQLite WAL-Mode Problem:**
- Die Datenbank ist im WAL-Mode konfiguriert (`PRAGMA journal_mode=WAL`)
- WAL-Mode erstellt `.wal` und `.shm` Dateien für bessere Performance
- Diese Dateien bleiben bestehen wenn keine ordnungsgemäße WAL-Checkpoint und Journal-Mode-Umschaltung beim Shutdown erfolgt
- Signal-Handler waren vorhanden, aber keine atexit-Handler für normales Programmende
### Implementierte Lösung
**1. Erweiterte Signal-Handler mit Datenbank-Cleanup:**
```python
def signal_handler(sig, frame):
# Queue Manager und Scheduler stoppen
# ===== DATENBANKVERBINDUNGEN ORDNUNGSGEMÄSS SCHLIESSEN =====
app_logger.info("💾 Führe Datenbank-Cleanup durch...")
try:
engine = create_optimized_engine()
with engine.connect() as conn:
# Vollständiger WAL-Checkpoint (TRUNCATE-Modus)
result = conn.execute(text("PRAGMA wal_checkpoint(TRUNCATE)")).fetchone()
# Journal-Mode zu DELETE wechseln (entfernt .wal/.shm Dateien)
conn.execute(text("PRAGMA journal_mode=DELETE"))
# Optimize und Vacuum für sauberen Zustand
conn.execute(text("PRAGMA optimize"))
conn.execute(text("VACUUM"))
conn.commit()
# Engine-Connection-Pool schließen
engine.dispose()
except Exception as db_error:
app_logger.error(f"❌ Fehler beim Datenbank-Cleanup: {str(db_error)}")
```
**2. Zusätzlicher atexit-Handler für normales Programmende:**
```python
def cleanup_database():
"""Führt Datenbank-Cleanup beim normalen Programmende aus."""
try:
engine = create_optimized_engine()
with engine.connect() as conn:
# WAL-Checkpoint für sauberes Beenden
result = conn.execute(text("PRAGMA wal_checkpoint(TRUNCATE)")).fetchone()
# Journal-Mode umschalten um .wal/.shm Dateien zu entfernen
conn.execute(text("PRAGMA journal_mode=DELETE"))
conn.commit()
# Connection-Pool ordnungsgemäß schließen
engine.dispose()
except Exception as e:
app_logger.error(f"❌ Fehler beim finalen Datenbank-Cleanup: {str(e)}")
atexit.register(cleanup_database)
```
### Funktionalität nach der Behebung
- ✅ **WAL-Checkpoint**: Vollständiger `PRAGMA wal_checkpoint(TRUNCATE)` überträgt alle WAL-Daten zurück in die Hauptdatenbank
- ✅ **Journal-Mode-Umschaltung**: `PRAGMA journal_mode=DELETE` entfernt die `.wal` und `.shm` Dateien
- ✅ **Engine-Cleanup**: `engine.dispose()` schließt alle Connection-Pools ordnungsgemäß
- ✅ **Signal-Handler**: Funktioniert bei Ctrl+C, SIGTERM, SIGBREAK (Windows)
- ✅ **atexit-Handler**: Funktioniert bei normalem Programmende
- ✅ **Fehlerbehandlung**: Detailliertes Logging für Debugging
- ✅ **Cross-Platform**: Windows und Unix/Linux kompatibel
### Ergebnis
✅ **Die `.shm` und `.wal` Dateien verschwinden jetzt ordnungsgemäß beim Beenden des Programms**
✅ **Robuste Datenbank-Cleanup-Mechanismen für alle Shutdown-Szenarien**
✅ **Bessere Performance durch behaltenen WAL-Mode während der Laufzeit**
✅ **Sauberer Dateisystem-Zustand nach Programmende**
**Status:** Problem vollständig behoben - SQLite WAL-Dateien werden automatisch aufgeräumt
---
## ✅ 30.05.2025 19:10 - Schnellaufträge-Funktionalität komplett repariert ## ✅ 30.05.2025 19:10 - Schnellaufträge-Funktionalität komplett repariert
### Problem ### Problem
@ -1200,4 +1295,110 @@ Die Standard-Browser-Scrollbalken waren zu aufdringlich und störten das elegant
**Status:** Design-Enhancement erfolgreich implementiert **Status:** Design-Enhancement erfolgreich implementiert
--- ## ✅ 30.05.2025 20:10 - Ultra-dezente Scrollbalken über base.html implementiert
### Problem
Die Scrollbalken in jobs.html waren immer noch zu auffällig. Benutzer wünschte ultra-dezente Scrollbalken die fast unsichtbar sind und nur bei Hover erscheinen.
### Lösung - Global über base.html Template
**Ultra-dezente Scrollbalken CSS direkt in `templates/base.html`:**
```css
/* ===== ULTRA-DEZENTE SCROLLBALKEN ===== */
/* Webkit-Browser (Chrome, Safari, Edge) */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: transparent;
border-radius: 3px;
transition: all 0.3s ease;
}
/* Nur bei Hover über scrollbaren Container sichtbar */
*:hover::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.05);
}
*:hover::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.1);
}
/* Dark Mode - noch dezenter */
.dark *:hover::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.03);
}
.dark *:hover::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.08);
}
/* Firefox - ultra-thin */
* {
scrollbar-width: none; /* Komplett versteckt in Firefox */
}
/* Nur bei Hover sichtbar in Firefox */
*:hover {
scrollbar-width: thin;
scrollbar-color: rgba(0, 0, 0, 0.05) transparent;
}
.dark *:hover {
scrollbar-color: rgba(255, 255, 255, 0.03) transparent;
}
/* Spezielle Container die scrollbar brauchen */
.modal-content::-webkit-scrollbar,
.dropdown-menu::-webkit-scrollbar {
width: 4px;
}
.modal-content::-webkit-scrollbar-thumb,
.dropdown-menu::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.1);
}
.dark .modal-content::-webkit-scrollbar-thumb,
.dark .dropdown-menu::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
}
```
### Features der neuen Implementation
1. **Ultra-dezent**: Nur 6px breit, komplett transparent bis Hover
2. **Hover-only Sichtbarkeit**: Scrollbalken erscheinen nur bei Hover über Container
3. **Noch schwächere Opacity**: rgba(0,0,0,0.05) vs vorher 0.1
4. **Dark Mode ultra-dezent**: rgba(255,255,255,0.03) - kaum sichtbar
5. **Firefox hidden by default**: `scrollbar-width: none` macht sie komplett unsichtbar
6. **Global Implementation**: Über base.html auf allen Seiten verfügbar
7. **Modals extra-schmal**: Nur 4px für beste UX
8. **Entfernt aus jobs.html**: Keine Dopplung mehr
### Vorteile
- ✅ **Fast unsichtbar**: Nur bei Hover schwach sichtbar
- ✅ **Nicht störend**: Design bleibt komplett clean
- ✅ **Global verfügbar**: Alle Seiten haben konsistente Scrollbalken
- ✅ **Performance**: Keine Dopplung von CSS-Regeln
- ✅ **Dark Mode optimiert**: Noch dezenter in dunklem Theme
- ✅ **Firefox clean**: Scrollbalken komplett versteckt bis Hover
### Ergebnis
✅ **Ultra-dezente Scrollbalken die praktisch unsichtbar sind**
✅ **Global über base.html implementiert - kein Duplicate CSS**
✅ **Erscheinen nur bei tatsächlichem Hover über scrollbare Container**
✅ **Design bleibt völlig clean und ungestört**
**Status:** Ultra-dezente Scrollbalken final implementiert

View File

@ -4860,3 +4860,178 @@ def get_printers():
"error": f"Fehler beim Laden der Drucker: {str(e)}", "error": f"Fehler beim Laden der Drucker: {str(e)}",
"printers": [] "printers": []
}), 500 }), 500
# ===== ERWEITERTE SESSION-MANAGEMENT UND AUTO-LOGOUT =====
@app.before_request
def check_session_activity():
"""
Überprüft Session-Aktivität und meldet Benutzer bei Inaktivität automatisch ab.
"""
# Skip für nicht-authentifizierte Benutzer und Login-Route
if not current_user.is_authenticated or request.endpoint in ['login', 'static', 'auth_logout']:
return
# Skip für AJAX/API calls die nicht als Session-Aktivität zählen sollen
if request.path.startswith('/api/') and request.path.endswith('/heartbeat'):
return
now = datetime.now()
# Session-Aktivität tracken
if 'last_activity' in session:
last_activity = datetime.fromisoformat(session['last_activity'])
inactive_duration = now - last_activity
# Definiere Inaktivitäts-Limits basierend auf Benutzerrolle
max_inactive_minutes = 30 # Standard: 30 Minuten
if hasattr(current_user, 'is_admin') and current_user.is_admin:
max_inactive_minutes = 60 # Admins: 60 Minuten
max_inactive_duration = timedelta(minutes=max_inactive_minutes)
# Benutzer abmelden wenn zu lange inaktiv
if inactive_duration > max_inactive_duration:
auth_logger.info(f"🕒 Automatische Abmeldung: Benutzer {current_user.email} war {inactive_duration.total_seconds()/60:.1f} Minuten inaktiv (Limit: {max_inactive_minutes}min)")
# Session-Daten vor Logout speichern für Benachrichtigung
session['auto_logout_reason'] = f"Automatische Abmeldung nach {max_inactive_minutes} Minuten Inaktivität"
session['auto_logout_time'] = now.isoformat()
logout_user()
session.clear()
# JSON-Response für AJAX-Requests
if request.headers.get('X-Requested-With') == 'XMLHttpRequest' or request.is_json:
return jsonify({
"error": "Session abgelaufen",
"reason": "auto_logout_inactivity",
"message": f"Sie wurden nach {max_inactive_minutes} Minuten Inaktivität automatisch abgemeldet",
"redirect_url": url_for("login")
}), 401
# HTML-Redirect für normale Requests
flash(f"Sie wurden nach {max_inactive_minutes} Minuten Inaktivität automatisch abgemeldet.", "warning")
return redirect(url_for("login"))
# Session-Aktivität aktualisieren (aber nicht bei jedem API-Call)
if not request.path.startswith('/api/stats/') and not request.path.startswith('/api/heartbeat'):
session['last_activity'] = now.isoformat()
session['user_agent'] = request.headers.get('User-Agent', '')[:200] # Begrenzt auf 200 Zeichen
session['ip_address'] = request.remote_addr
# Session-Sicherheit: Überprüfe IP-Adresse und User-Agent (Optional)
if 'session_ip' in session and session['session_ip'] != request.remote_addr:
auth_logger.warning(f"⚠️ IP-Adresse geändert für Benutzer {current_user.email}: {session['session_ip']}{request.remote_addr}")
# Optional: Benutzer abmelden bei IP-Wechsel (kann bei VPN/Proxy problematisch sein)
# session['security_warning'] = "IP-Adresse hat sich geändert"
@app.before_request
def setup_session_security():
"""
Initialisiert Session-Sicherheit für neue Sessions.
"""
if current_user.is_authenticated and 'session_created' not in session:
session['session_created'] = datetime.now().isoformat()
session['session_ip'] = request.remote_addr
session['last_activity'] = datetime.now().isoformat()
session.permanent = True # Session als permanent markieren
auth_logger.info(f"🔐 Neue Session erstellt für Benutzer {current_user.email} von IP {request.remote_addr}")
# ===== SESSION-MANAGEMENT API-ENDPUNKTE =====
@app.route('/api/session/heartbeat', methods=['POST'])
@login_required
def session_heartbeat():
"""
Heartbeat-Endpunkt um Session am Leben zu halten.
Wird vom Frontend alle 5 Minuten aufgerufen.
"""
try:
now = datetime.now()
session['last_activity'] = now.isoformat()
# Berechne verbleibende Session-Zeit
last_activity = datetime.fromisoformat(session.get('last_activity', now.isoformat()))
max_inactive_minutes = 60 if hasattr(current_user, 'is_admin') and current_user.is_admin else 30
time_left = max_inactive_minutes * 60 - (now - last_activity).total_seconds()
return jsonify({
"success": True,
"session_active": True,
"time_left_seconds": max(0, int(time_left)),
"max_inactive_minutes": max_inactive_minutes,
"current_time": now.isoformat()
})
except Exception as e:
auth_logger.error(f"Fehler beim Session-Heartbeat: {str(e)}")
return jsonify({"error": "Heartbeat fehlgeschlagen"}), 500
@app.route('/api/session/status', methods=['GET'])
@login_required
def session_status():
"""
Gibt detaillierten Session-Status zurück.
"""
try:
now = datetime.now()
last_activity = datetime.fromisoformat(session.get('last_activity', now.isoformat()))
session_created = datetime.fromisoformat(session.get('session_created', now.isoformat()))
max_inactive_minutes = 60 if hasattr(current_user, 'is_admin') and current_user.is_admin else 30
inactive_duration = (now - last_activity).total_seconds()
time_left = max_inactive_minutes * 60 - inactive_duration
return jsonify({
"success": True,
"user": {
"id": current_user.id,
"email": current_user.email,
"name": current_user.name,
"is_admin": getattr(current_user, 'is_admin', False)
},
"session": {
"created": session_created.isoformat(),
"last_activity": last_activity.isoformat(),
"inactive_seconds": int(inactive_duration),
"time_left_seconds": max(0, int(time_left)),
"max_inactive_minutes": max_inactive_minutes,
"ip_address": session.get('session_ip', 'unbekannt'),
"user_agent": session.get('user_agent', 'unbekannt')[:50] + "..." if len(session.get('user_agent', '')) > 50 else session.get('user_agent', 'unbekannt')
},
"warnings": []
})
except Exception as e:
auth_logger.error(f"Fehler beim Abrufen des Session-Status: {str(e)}")
return jsonify({"error": "Session-Status nicht verfügbar"}), 500
@app.route('/api/session/extend', methods=['POST'])
@login_required
def extend_session():
"""
Verlängert die aktuelle Session um weitere Zeit.
"""
try:
data = request.get_json() or {}
extend_minutes = data.get('extend_minutes', 30)
# Begrenzen der Verlängerung (max 2 Stunden)
extend_minutes = min(extend_minutes, 120)
now = datetime.now()
session['last_activity'] = now.isoformat()
session['session_extended'] = now.isoformat()
session['extended_by_minutes'] = extend_minutes
auth_logger.info(f"🕒 Session verlängert für Benutzer {current_user.email} um {extend_minutes} Minuten")
return jsonify({
"success": True,
"message": f"Session um {extend_minutes} Minuten verlängert",
"extended_until": (now + timedelta(minutes=extend_minutes)).isoformat(),
"extended_minutes": extend_minutes
})
except Exception as e:
auth_logger.error(f"Fehler beim Verlängern der Session: {str(e)}")
return jsonify({"error": "Session-Verlängerung fehlgeschlagen"}), 500

View File

@ -67,7 +67,7 @@ FLASK_HOST = "0.0.0.0"
FLASK_PORT = 443 # Geändert von 443 auf 8443 (nicht-privilegierter Port) FLASK_PORT = 443 # Geändert von 443 auf 8443 (nicht-privilegierter Port)
FLASK_FALLBACK_PORT = 8080 # Geändert von 80 auf 8080 (nicht-privilegierter Port) FLASK_FALLBACK_PORT = 8080 # Geändert von 80 auf 8080 (nicht-privilegierter Port)
FLASK_DEBUG = True FLASK_DEBUG = True
SESSION_LIFETIME = timedelta(days=7) SESSION_LIFETIME = timedelta(hours=2) # Reduziert von 7 Tagen auf 2 Stunden für bessere Sicherheit
# Upload-Konfiguration # Upload-Konfiguration
UPLOAD_FOLDER = os.path.join(BASE_DIR, "uploads") UPLOAD_FOLDER = os.path.join(BASE_DIR, "uploads")

View File

@ -0,0 +1,475 @@
/**
* Erweitertes Session-Management für MYP Platform
*
* Features:
* - Automatische Session-Überwachung
* - Heartbeat-System für Session-Verlängerung
* - Benutzer-Warnungen bei bevorstehender Abmeldung
* - Graceful Logout bei Session-Ablauf
* - Modal-Dialoge für Session-Verlängerung
*
* @author Mercedes-Benz MYP Platform
* @version 2.0
*/
class SessionManager {
constructor() {
this.isAuthenticated = false;
this.maxInactiveMinutes = 30; // Standard: 30 Minuten
this.heartbeatInterval = 5 * 60 * 1000; // 5 Minuten
this.warningTime = 5 * 60 * 1000; // 5 Minuten vor Ablauf warnen
this.checkInterval = 30 * 1000; // Alle 30 Sekunden prüfen
this.heartbeatTimer = null;
this.statusCheckTimer = null;
this.warningShown = false;
this.sessionWarningModal = null;
this.init();
}
async init() {
try {
// Prüfe initial ob Benutzer angemeldet ist
await this.checkAuthenticationStatus();
if (this.isAuthenticated) {
this.startSessionMonitoring();
this.createWarningModal();
console.log('🔐 Session Manager gestartet');
console.log(`📊 Max Inaktivität: ${this.maxInactiveMinutes} Minuten`);
console.log(`💓 Heartbeat Intervall: ${this.heartbeatInterval / 1000 / 60} Minuten`);
} else {
console.log('👤 Benutzer nicht angemeldet - Session Manager inaktiv');
}
} catch (error) {
console.error('❌ Session Manager Initialisierung fehlgeschlagen:', error);
}
}
async checkAuthenticationStatus() {
try {
const response = await fetch('/api/session/status', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
});
if (response.ok) {
const data = await response.json();
if (data.success) {
this.isAuthenticated = true;
this.maxInactiveMinutes = data.session.max_inactive_minutes;
console.log('✅ Session Status:', {
user: data.user.email,
timeLeft: Math.floor(data.session.time_left_seconds / 60) + ' Minuten',
lastActivity: new Date(data.session.last_activity).toLocaleString('de-DE')
});
return data;
}
} else if (response.status === 401) {
this.isAuthenticated = false;
this.handleSessionExpired('Authentication check failed');
}
} catch (error) {
console.error('❌ Fehler beim Prüfen des Session-Status:', error);
this.isAuthenticated = false;
}
return null;
}
startSessionMonitoring() {
// Heartbeat alle 5 Minuten senden
this.heartbeatTimer = setInterval(() => {
this.sendHeartbeat();
}, this.heartbeatInterval);
// Session-Status alle 30 Sekunden prüfen
this.statusCheckTimer = setInterval(() => {
this.checkSessionStatus();
}, this.checkInterval);
// Initial Heartbeat senden
setTimeout(() => this.sendHeartbeat(), 1000);
}
async sendHeartbeat() {
try {
const response = await fetch('/api/session/heartbeat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({
timestamp: new Date().toISOString(),
page: window.location.pathname
})
});
if (response.ok) {
const data = await response.json();
if (data.success) {
console.log('💓 Heartbeat gesendet - Session aktiv:',
Math.floor(data.time_left_seconds / 60) + ' Minuten verbleibend');
} else {
console.warn('⚠️ Heartbeat fehlgeschlagen:', data);
}
} else if (response.status === 401) {
this.handleSessionExpired('Heartbeat failed - unauthorized');
}
} catch (error) {
console.error('❌ Heartbeat-Fehler:', error);
}
}
async checkSessionStatus() {
try {
const sessionData = await this.checkAuthenticationStatus();
if (sessionData && sessionData.session) {
const timeLeftSeconds = sessionData.session.time_left_seconds;
const timeLeftMinutes = Math.floor(timeLeftSeconds / 60);
// Warnung anzeigen wenn weniger als 5 Minuten verbleiben
if (timeLeftSeconds <= this.warningTime / 1000 && timeLeftSeconds > 0) {
if (!this.warningShown) {
this.showSessionWarning(timeLeftMinutes);
this.warningShown = true;
}
} else if (timeLeftSeconds <= 0) {
// Session abgelaufen
this.handleSessionExpired('Session time expired');
} else {
// Session OK - Warnung zurücksetzen
this.warningShown = false;
this.hideSessionWarning();
}
// Session-Status in der UI aktualisieren
this.updateSessionStatusDisplay(sessionData);
}
} catch (error) {
console.error('❌ Session-Status-Check fehlgeschlagen:', error);
}
}
showSessionWarning(minutesLeft) {
// Bestehende Warnung entfernen
this.hideSessionWarning();
// Toast-Notification anzeigen
this.showToast(
'Session läuft ab',
`Ihre Session läuft in ${minutesLeft} Minuten ab. Möchten Sie verlängern?`,
'warning',
10000, // 10 Sekunden anzeigen
[
{
text: 'Verlängern',
action: () => this.extendSession()
},
{
text: 'Abmelden',
action: () => this.logout()
}
]
);
// Modal anzeigen für wichtige Warnung
if (this.sessionWarningModal) {
this.sessionWarningModal.show();
this.updateWarningModal(minutesLeft);
}
console.log(`⚠️ Session-Warnung: ${minutesLeft} Minuten verbleibend`);
}
hideSessionWarning() {
if (this.sessionWarningModal) {
this.sessionWarningModal.hide();
}
}
createWarningModal() {
// Modal HTML erstellen
const modalHTML = `
<div id="sessionWarningModal" class="fixed inset-0 z-50 hidden overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>
<div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<svg class="h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L3.314 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-title">
Session läuft ab
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500" id="warningMessage">
Ihre Session läuft in <span id="timeRemaining" class="font-bold text-red-600">5</span> Minuten ab.
</p>
<p class="text-sm text-gray-500 mt-2">
Möchten Sie Ihre Session verlängern oder sich abmelden?
</p>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button type="button" id="extendSessionBtn" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
Session verlängern
</button>
<button type="button" id="logoutBtn" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
Abmelden
</button>
</div>
</div>
</div>
</div>`;
// Modal in DOM einfügen
document.body.insertAdjacentHTML('beforeend', modalHTML);
// Event-Listener hinzufügen
document.getElementById('extendSessionBtn').addEventListener('click', () => {
this.extendSession();
this.hideSessionWarning();
});
document.getElementById('logoutBtn').addEventListener('click', () => {
this.logout();
});
// Modal-Objekt erstellen
this.sessionWarningModal = {
element: document.getElementById('sessionWarningModal'),
show: () => {
document.getElementById('sessionWarningModal').classList.remove('hidden');
},
hide: () => {
document.getElementById('sessionWarningModal').classList.add('hidden');
}
};
}
updateWarningModal(minutesLeft) {
const timeElement = document.getElementById('timeRemaining');
if (timeElement) {
timeElement.textContent = minutesLeft;
}
}
async extendSession(extendMinutes = 30) {
try {
const response = await fetch('/api/session/extend', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({
extend_minutes: extendMinutes
})
});
if (response.ok) {
const data = await response.json();
if (data.success) {
this.warningShown = false;
this.showToast(
'Session verlängert',
`Ihre Session wurde um ${data.extended_minutes} Minuten verlängert`,
'success',
5000
);
console.log('✅ Session verlängert:', data);
} else {
this.showToast('Fehler', 'Session konnte nicht verlängert werden', 'error');
}
} else if (response.status === 401) {
this.handleSessionExpired('Extend session failed - unauthorized');
}
} catch (error) {
console.error('❌ Session-Verlängerung fehlgeschlagen:', error);
this.showToast('Fehler', 'Session-Verlängerung fehlgeschlagen', 'error');
}
}
async logout() {
try {
this.stopSessionMonitoring();
// Logout-Request senden
const response = await fetch('/auth/logout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
});
// Zur Login-Seite weiterleiten
if (response.ok) {
window.location.href = '/auth/login';
} else {
// Fallback: Direkter Redirect
window.location.href = '/auth/login';
}
} catch (error) {
console.error('❌ Logout-Fehler:', error);
// Fallback: Direkter Redirect
window.location.href = '/auth/login';
}
}
handleSessionExpired(reason) {
console.log('🕒 Session abgelaufen:', reason);
this.stopSessionMonitoring();
this.isAuthenticated = false;
// Benutzer benachrichtigen
this.showToast(
'Session abgelaufen',
'Sie wurden automatisch abgemeldet. Bitte melden Sie sich erneut an.',
'warning',
8000
);
// Nach kurzer Verzögerung zur Login-Seite weiterleiten
setTimeout(() => {
window.location.href = '/auth/login?reason=session_expired';
}, 2000);
}
stopSessionMonitoring() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
if (this.statusCheckTimer) {
clearInterval(this.statusCheckTimer);
this.statusCheckTimer = null;
}
console.log('🛑 Session-Monitoring gestoppt');
}
updateSessionStatusDisplay(sessionData) {
// Session-Status in der Navigation/Header anzeigen (falls vorhanden)
const statusElement = document.getElementById('sessionStatus');
if (statusElement) {
const timeLeftMinutes = Math.floor(sessionData.session.time_left_seconds / 60);
statusElement.textContent = `Session: ${timeLeftMinutes}min`;
// Farbe basierend auf verbleibender Zeit
if (timeLeftMinutes <= 5) {
statusElement.className = 'text-red-600 font-medium';
} else if (timeLeftMinutes <= 10) {
statusElement.className = 'text-yellow-600 font-medium';
} else {
statusElement.className = 'text-green-600 font-medium';
}
}
}
showToast(title, message, type = 'info', duration = 5000, actions = []) {
// Verwende das bestehende Toast-System falls verfügbar
if (window.showToast) {
window.showToast(message, type, duration);
return;
}
// Fallback: Simple Browser-Notification
if (type === 'error' || type === 'warning') {
alert(`${title}: ${message}`);
} else {
console.log(`${title}: ${message}`);
}
}
// === ÖFFENTLICHE API ===
/**
* Prüft ob Benutzer angemeldet ist
*/
isLoggedIn() {
return this.isAuthenticated;
}
/**
* Startet Session-Monitoring manuell
*/
start() {
if (!this.heartbeatTimer && this.isAuthenticated) {
this.startSessionMonitoring();
}
}
/**
* Stoppt Session-Monitoring manuell
*/
stop() {
this.stopSessionMonitoring();
}
/**
* Verlängert Session manuell
*/
async extend(minutes = 30) {
return await this.extendSession(minutes);
}
/**
* Meldet Benutzer manuell ab
*/
async logoutUser() {
return await this.logout();
}
}
// Session Manager automatisch starten wenn DOM geladen ist
document.addEventListener('DOMContentLoaded', () => {
// Nur starten wenn wir nicht auf der Login-Seite sind
if (!window.location.pathname.includes('/auth/login')) {
window.sessionManager = new SessionManager();
// Globale Event-Listener für Session-Management
window.addEventListener('beforeunload', () => {
if (window.sessionManager) {
window.sessionManager.stop();
}
});
// Reaktion auf Sichtbarkeitsänderungen (Tab-Wechsel)
document.addEventListener('visibilitychange', () => {
if (window.sessionManager && window.sessionManager.isLoggedIn()) {
if (document.hidden) {
console.log('🙈 Tab versteckt - Session-Monitoring reduziert');
} else {
console.log('👁️ Tab sichtbar - Session-Check');
// Sofortiger Session-Check wenn Tab wieder sichtbar wird
setTimeout(() => window.sessionManager.checkSessionStatus(), 1000);
}
}
});
}
});
// Session Manager für andere Scripts verfügbar machen
window.SessionManager = SessionManager;

View File

@ -654,6 +654,7 @@
<script src="{{ url_for('static', filename='js/printer_monitor.js') }}"></script> <script src="{{ url_for('static', filename='js/printer_monitor.js') }}"></script>
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
<script src="{{ url_for('static', filename='js/notifications.js') }}"></script> <script src="{{ url_for('static', filename='js/notifications.js') }}"></script>
<script src="{{ url_for('static', filename='js/session-manager.js') }}"></script>
<script src="{{ url_for('static', filename='js/auto-logout.js') }}"></script> <script src="{{ url_for('static', filename='js/auto-logout.js') }}"></script>
{% endif %} {% endif %}