From 9c3d58b04fc22b49aec0e5bc00f52fa73b6fd630 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Mon, 22 Jun 2026 12:34:38 +0200 Subject: [PATCH] Refactor (Welle A): Pins, Konstanten, gezieltes CSRF, robustes load_lessons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - requirements.txt: exakte Versions-Pins; fehlendes 'markdown' ergänzt (wird in routes/main.py importiert) - statistics_service: Magic Numbers (max ZPM/WPM, Mindestzeit, Wortlänge) als benannte Konstanten - app.py: CSRF-Ausnahme gezielt nur für /shutdown statt ganzes utils-Blueprint -> /repair_json und /recreate_lessons bleiben geschützt - helpers.load_lessons: automatische JSON-Reparatur-Heuristik entfernt; bei Fehler laut loggen + In-Memory-Fallback, fehlerhafte Datei bleibt unverändert (behebt latenten Datenverlust durch Überschreiben). Manueller Endpoint POST /repair_json bleibt erhalten. Co-Authored-By: Claude Sonnet 4.6 --- app.py | 7 ++++--- requirements.txt | 9 +++++---- services/helpers.py | 37 +++++++++------------------------- services/statistics_service.py | 18 +++++++++++------ 4 files changed, 31 insertions(+), 40 deletions(-) diff --git a/app.py b/app.py index 0167eed..0974584 100644 --- a/app.py +++ b/app.py @@ -49,7 +49,7 @@ from routes.training import training_bp from routes.statistics import statistics_bp from routes.settings import settings_bp from routes.session import session_bp -from routes.utils import utils_bp +from routes.utils import utils_bp, shutdown app.register_blueprint(main_bp) app.register_blueprint(training_bp) @@ -58,8 +58,9 @@ app.register_blueprint(settings_bp) app.register_blueprint(session_bp) app.register_blueprint(utils_bp) -# CSRF-Ausnahme für Shutdown-Route -csrf.exempt(utils_bp) +# CSRF-Ausnahme gezielt nur für die Shutdown-Route. +# /repair_json und /recreate_lessons bleiben damit CSRF-geschützt. +csrf.exempt(shutdown) # Datenbank-Status prüfen und Lektionen laden (nur wenn nicht im Test-Modus) diff --git a/requirements.txt b/requirements.txt index b3c9644..be6d0f3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ -flask>=2.3.0 -flask-sqlalchemy>=3.0.0 -flask-wtf>=1.2.0 -alembic>=1.13.0 +Flask==3.1.2 +Flask-SQLAlchemy==3.1.1 +Flask-WTF==1.2.2 +alembic==1.17.0 +Markdown==3.9 diff --git a/services/helpers.py b/services/helpers.py index 5d7ace1..437297e 100644 --- a/services/helpers.py +++ b/services/helpers.py @@ -120,38 +120,21 @@ def load_lessons() -> None: with open(DATA_PATH, "r", encoding="utf-8") as f: content = f.read().strip() - # Versuche zuerst das originale JSON zu laden + # JSON laden. Bei Fehler NICHT automatisch raten/überschreiben, + # sondern laut scheitern und In-Memory-Fallback nutzen. Die + # fehlerhafte Datei bleibt für Diagnose/Reparatur unverändert. try: lessons = json.loads(content) logger.info(f"Erfolgreich {len(lessons)} Lektionen aus {DATA_PATH} geladen") return except json.JSONDecodeError as e: - logger.error(f"JSON-Fehler in {DATA_PATH}: {e}") - logger.error(f"Position: Zeile {e.lineno}, Spalte {e.colno}") - - # Versuche das JSON zu reparieren - logger.info("Versuche JSON automatisch zu reparieren...") - repaired_content = repair_json(content) - - try: - lessons = json.loads(repaired_content) - logger.info(f"Erfolgreich {len(lessons)} Lektionen nach Reparatur geladen") - - # Speichere die reparierte Version als Backup - backup_path = DATA_PATH + ".repaired" - with open(backup_path, "w", encoding="utf-8") as f: - json.dump(lessons, f, indent=2, ensure_ascii=False) - logger.info(f"Reparierte Version gespeichert als: {backup_path}") - return - - except json.JSONDecodeError as e2: - logger.error(f"Automatische Reparatur fehlgeschlagen: {e2}") - logger.warning("Verwende Fallback-Lektionen und erstelle neue Lektionen-Datei") - lessons = FALLBACK_LESSONS - # Speichere Fallback-Lektionen für zukünftige Verwendung - with open(DATA_PATH, "w", encoding="utf-8") as f: - json.dump(lessons, f, indent=2, ensure_ascii=False) - logger.info(f"Fallback-Lektionen gespeichert in: {DATA_PATH}") + logger.error(f"JSON-Fehler in {DATA_PATH}: Zeile {e.lineno}, Spalte {e.colno}: {e.msg}") + logger.warning( + "Verwende Fallback-Lektionen (in-memory). Die fehlerhafte Datei " + "bleibt unverändert; zur Reparatur den Endpoint POST /repair_json nutzen." + ) + lessons = FALLBACK_LESSONS + return else: logger.warning(f"Datei {DATA_PATH} nicht gefunden. Verwende Fallback-Lektionen und erstelle Datei.") lessons = FALLBACK_LESSONS diff --git a/services/statistics_service.py b/services/statistics_service.py index 971dd37..6516ffb 100644 --- a/services/statistics_service.py +++ b/services/statistics_service.py @@ -10,6 +10,12 @@ from models import db, LessonStatistic, DailyPractice logger = logging.getLogger(__name__) +# Konstanten für die Statistik-Berechnung +MIN_ELAPSED_TIME_SECONDS = 1.0 # Mindestzeit; darunter unrealistische Werte +MAX_CHARS_PER_MINUTE = 1000 # Plausibilitäts-Obergrenze für ZPM +MAX_WORDS_PER_MINUTE = 200 # Plausibilitäts-Obergrenze für WPM +CHARS_PER_WORD = 5 # Standard-Wortlänge für die WPM-Berechnung + class StatisticsService: """Service for managing statistics""" @@ -43,18 +49,18 @@ class StatisticsService: incorrect_chars += 1 # Calculate metrics based on real time - # Minimum 1 second to avoid unrealistic values - if total_chars > 0 and elapsed_time >= 1.0: + # Minimum time to avoid unrealistic values + if total_chars > 0 and elapsed_time >= MIN_ELAPSED_TIME_SECONDS: # Characters per minute (integer) chars_per_minute = (total_chars / elapsed_time) * 60 - # Words per minute: (correct characters / 5) / (time in minutes) - words_per_minute = (correct_chars / 5) / (elapsed_time / 60) + # Words per minute: (correct characters / CHARS_PER_WORD) / (time in minutes) + words_per_minute = (correct_chars / CHARS_PER_WORD) / (elapsed_time / 60) # Average time per character duration_per_char = elapsed_time / total_chars # Limit to realistic maximum values - chars_per_minute = min(chars_per_minute, 1000) - words_per_minute = min(words_per_minute, 200) + chars_per_minute = min(chars_per_minute, MAX_CHARS_PER_MINUTE) + words_per_minute = min(words_per_minute, MAX_WORDS_PER_MINUTE) else: chars_per_minute = 0 words_per_minute = 0