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:
122
app.py
122
app.py
@@ -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)
|
||||
|
Reference in New Issue
Block a user