Admin: Passwortgeschütztes Statistik-Dashboard implementiert

- Neue Routen: /login, /stats, /logout
- Session-basierte Authentifizierung
- Umfassende Statistiken: Seitenaufrufe, Suchvorgänge, Quellen
- Environment-Variablen: ADMIN_PASSWORD, FLASK_SECRET_KEY
- Docker-Integration mit docker-compose.yml
- Responsive UI mit Charts und Aktivitätsprotokoll
This commit is contained in:
2025-08-20 09:11:53 +02:00
parent fcbcb07e76
commit d730e6b266
5 changed files with 525 additions and 2 deletions

122
app.py
View File

@@ -4,12 +4,16 @@ import logging
import os
from datetime import datetime, timedelta
from typing import Tuple, Dict, List
from flask import Flask, render_template, request, send_from_directory
from flask import Flask, render_template, request, send_from_directory, session, redirect, url_for, flash
from functools import wraps
app = Flask(__name__)
app.secret_key = os.environ.get('FLASK_SECRET_KEY', 'dev-secret-key-change-in-production')
# Admin-Passwort aus Environment-Variable
ADMIN_PASSWORD = os.environ.get('ADMIN_PASSWORD', 'admin123')
# Logging konfigurieren
import os
# Logs-Verzeichnis erstellen, falls es nicht existiert
logs_dir = Path(__file__).parent / "logs"
@@ -48,6 +52,84 @@ def log_search_query(search_params: dict, user_agent: str = None):
logger.info(f"SEARCH: pos='{pos_str}' includes='{includes}' excludes='{excludes}' sources={sources} | User-Agent: {user_agent_clean}")
def login_required(f):
"""Decorator für passwortgeschützte Routen"""
@wraps(f)
def decorated_function(*args, **kwargs):
if not session.get('logged_in'):
return redirect(url_for('login'))
return f(*args, **kwargs)
return decorated_function
def get_statistics():
"""Liest und analysiert die Log-Dateien für Statistiken"""
stats = {
'total_page_views': 0,
'total_searches': 0,
'page_views_by_page': {},
'searches_by_source': {'OT': 0, 'WF': 0, 'Both': 0},
'recent_activity': [],
'top_search_patterns': {}
}
try:
# Aktuelle Log-Datei lesen
log_file = logs_dir / 'app.log'
if log_file.exists():
with open(log_file, 'r', encoding='utf-8') as f:
for line in f:
if 'PAGE_VIEW:' in line:
stats['total_page_views'] += 1
# Seite extrahieren
if 'PAGE_VIEW: ' in line:
page = line.split('PAGE_VIEW: ')[1].split(' |')[0]
stats['page_views_by_page'][page] = stats['page_views_by_page'].get(page, 0) + 1
elif 'SEARCH:' in line:
stats['total_searches'] += 1
# Quellen extrahieren
if 'sources=[' in line:
sources_part = line.split('sources=')[1].split(']')[0]
if 'OT' in sources_part and 'WF' in sources_part:
stats['searches_by_source']['Both'] += 1
elif 'OT' in sources_part:
stats['searches_by_source']['OT'] += 1
elif 'WF' in sources_part:
stats['searches_by_source']['WF'] += 1
# Suchmuster extrahieren
if 'pos=' in line:
pos_part = line.split('pos=\'')[1].split('\'')[0]
if pos_part:
stats['top_search_patterns'][pos_part] = stats['top_search_patterns'].get(pos_part, 0) + 1
# Letzte 10 Aktivitäten
if len(stats['recent_activity']) < 10:
timestamp = line.split(' - ')[0] if ' - ' in line else ''
if timestamp:
stats['recent_activity'].append({
'timestamp': timestamp,
'line': line.strip()
})
# Backup-Dateien auch durchsuchen
for backup_file in logs_dir.glob("app_*.log.gz"):
try:
import gzip
with gzip.open(backup_file, 'rt', encoding='utf-8') as f:
for line in f:
if 'PAGE_VIEW:' in line:
stats['total_page_views'] += 1
elif 'SEARCH:' in line:
stats['total_searches'] += 1
except Exception as e:
logger.error(f"Fehler beim Lesen der Backup-Datei {backup_file}: {e}")
except Exception as e:
logger.error(f"Fehler beim Lesen der Statistiken: {e}")
return stats
def cleanup_old_logs():
"""Bereinigt Log-Dateien älter als 7 Tage"""
try:
@@ -201,5 +283,41 @@ def service_worker():
return send_from_directory(Path(__file__).parent / 'static', 'sw.js', mimetype='application/javascript')
@app.route('/login', methods=['GET', 'POST'])
def login():
"""Login-Seite für das Admin-Dashboard"""
if request.method == 'POST':
password = request.form.get('password')
logger.info(f"Login-Versuch: Passwort-Länge: {len(password) if password else 0}, ADMIN_PASSWORD gesetzt: {bool(ADMIN_PASSWORD)}")
if password == ADMIN_PASSWORD:
session['logged_in'] = True
flash('Erfolgreich angemeldet!', 'success')
logger.info("Login erfolgreich")
return redirect(url_for('stats'))
else:
flash('Falsches Passwort!', 'error')
logger.warning(f"Login fehlgeschlagen - eingegebenes Passwort: '{password}', erwartetes: '{ADMIN_PASSWORD}'")
return render_template('login.html')
@app.route('/logout')
def logout():
"""Logout-Funktion"""
session.pop('logged_in', None)
flash('Erfolgreich abgemeldet!', 'success')
return redirect(url_for('index'))
@app.route('/stats')
@login_required
def stats():
"""Statistik-Dashboard (passwortgeschützt)"""
log_page_view("stats", request.headers.get('User-Agent'))
statistics = get_statistics()
return render_template('stats.html', stats=statistics)
if __name__ == "__main__":
logger.info(f"App gestartet - ADMIN_PASSWORD gesetzt: {bool(ADMIN_PASSWORD)}, Länge: {len(ADMIN_PASSWORD) if ADMIN_PASSWORD else 0}")
app.run(debug=True)