📚 Improved error handling documentation and codebase organization in the frontend application. 🖥️🔍
This commit is contained in:
parent
d1a6281577
commit
0d966712a7
@ -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
|
||||
|
||||
### Problem
|
||||
@ -1200,4 +1295,110 @@ Die Standard-Browser-Scrollbalken waren zu aufdringlich und störten das elegant
|
||||
|
||||
**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
|
@ -343,11 +343,11 @@ def login():
|
||||
password = request.form.get("password")
|
||||
remember_me = request.form.get("remember_me") == "on"
|
||||
|
||||
# Zusätzlicher Fallback für verschiedene Feldnamen
|
||||
if not username:
|
||||
username = request.form.get("username") or request.values.get("email") or request.values.get("username")
|
||||
if not password:
|
||||
password = request.form.get("password") or request.values.get("password")
|
||||
# Zusätzlicher Fallback für verschiedene Feldnamen
|
||||
if not username:
|
||||
username = request.form.get("username") or request.values.get("email") or request.values.get("username")
|
||||
if not password:
|
||||
password = request.form.get("password") or request.values.get("password")
|
||||
|
||||
except Exception as extract_error:
|
||||
auth_logger.error(f"Fehler beim Extrahieren der Login-Daten: {str(extract_error)}")
|
||||
@ -2299,16 +2299,16 @@ def finish_job(job_id):
|
||||
def cancel_job(job_id):
|
||||
"""Bricht einen Job ab."""
|
||||
try:
|
||||
db_session = get_db_session()
|
||||
db_session = get_db_session()
|
||||
job = db_session.query(Job).get(job_id)
|
||||
|
||||
if not job:
|
||||
db_session.close()
|
||||
db_session.close()
|
||||
return jsonify({"error": "Job nicht gefunden"}), 404
|
||||
|
||||
# Prüfen, ob der Job abgebrochen werden kann
|
||||
if job.status not in ["scheduled", "running"]:
|
||||
db_session.close()
|
||||
db_session.close()
|
||||
return jsonify({"error": f"Job kann im Status '{job.status}' nicht abgebrochen werden"}), 400
|
||||
|
||||
# Job als abgebrochen markieren
|
||||
@ -2320,7 +2320,7 @@ def cancel_job(job_id):
|
||||
from utils.job_scheduler import toggle_plug
|
||||
toggle_plug(job.printer_id, False)
|
||||
|
||||
db_session.commit()
|
||||
db_session.commit()
|
||||
|
||||
job_dict = job.to_dict()
|
||||
db_session.close()
|
||||
@ -2338,7 +2338,7 @@ def cancel_job(job_id):
|
||||
def start_job(job_id):
|
||||
"""Startet einen Job manuell."""
|
||||
try:
|
||||
db_session = get_db_session()
|
||||
db_session = get_db_session()
|
||||
job = db_session.query(Job).options(joinedload(Job.printer)).get(job_id)
|
||||
|
||||
if not job:
|
||||
@ -2347,7 +2347,7 @@ def start_job(job_id):
|
||||
|
||||
# Prüfen, ob der Job gestartet werden kann
|
||||
if job.status not in ["scheduled", "queued", "waiting_for_printer"]:
|
||||
db_session.close()
|
||||
db_session.close()
|
||||
return jsonify({"error": f"Job kann im Status '{job.status}' nicht gestartet werden"}), 400
|
||||
|
||||
# Drucker einschalten falls verfügbar
|
||||
@ -2358,7 +2358,7 @@ def start_job(job_id):
|
||||
jobs_logger.info(f"Drucker {job.printer.name} für Job {job_id} eingeschaltet")
|
||||
else:
|
||||
jobs_logger.warning(f"Konnte Drucker {job.printer.name} für Job {job_id} nicht einschalten")
|
||||
except Exception as e:
|
||||
except Exception as e:
|
||||
jobs_logger.warning(f"Fehler beim Einschalten des Druckers für Job {job_id}: {str(e)}")
|
||||
|
||||
# Job als laufend markieren
|
||||
@ -2398,7 +2398,7 @@ def pause_job(job_id):
|
||||
|
||||
# Prüfen, ob der Job pausiert werden kann
|
||||
if job.status != "running":
|
||||
db_session.close()
|
||||
db_session.close()
|
||||
return jsonify({"error": f"Job kann im Status '{job.status}' nicht pausiert werden"}), 400
|
||||
|
||||
# Drucker ausschalten
|
||||
@ -2409,7 +2409,7 @@ def pause_job(job_id):
|
||||
jobs_logger.info(f"Drucker {job.printer.name} für Job {job_id} ausgeschaltet (Pause)")
|
||||
else:
|
||||
jobs_logger.warning(f"Konnte Drucker {job.printer.name} für Job {job_id} nicht ausschalten")
|
||||
except Exception as e:
|
||||
except Exception as e:
|
||||
jobs_logger.warning(f"Fehler beim Ausschalten des Druckers für Job {job_id}: {str(e)}")
|
||||
|
||||
# Job als pausiert markieren
|
||||
@ -2419,7 +2419,7 @@ def pause_job(job_id):
|
||||
db_session.commit()
|
||||
|
||||
job_dict = job.to_dict()
|
||||
db_session.close()
|
||||
db_session.close()
|
||||
|
||||
jobs_logger.info(f"Job {job_id} pausiert von Benutzer {current_user.id}")
|
||||
return jsonify({
|
||||
@ -2458,7 +2458,7 @@ def resume_job(job_id):
|
||||
jobs_logger.info(f"Drucker {job.printer.name} für Job {job_id} eingeschaltet (Resume)")
|
||||
else:
|
||||
jobs_logger.warning(f"Konnte Drucker {job.printer.name} für Job {job_id} nicht einschalten")
|
||||
except Exception as e:
|
||||
except Exception as e:
|
||||
jobs_logger.warning(f"Fehler beim Einschalten des Druckers für Job {job_id}: {str(e)}")
|
||||
|
||||
# Job als laufend markieren
|
||||
@ -4859,4 +4859,179 @@ def get_printers():
|
||||
return jsonify({
|
||||
"error": f"Fehler beim Laden der Drucker: {str(e)}",
|
||||
"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
|
@ -67,7 +67,7 @@ FLASK_HOST = "0.0.0.0"
|
||||
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_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_FOLDER = os.path.join(BASE_DIR, "uploads")
|
||||
|
475
backend/app/static/js/session-manager.js
Normal file
475
backend/app/static/js/session-manager.js
Normal 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;
|
@ -654,6 +654,7 @@
|
||||
<script src="{{ url_for('static', filename='js/printer_monitor.js') }}"></script>
|
||||
{% if current_user.is_authenticated %}
|
||||
<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>
|
||||
{% endif %}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user