Refactor (Welle A): Pins, Konstanten, gezieltes CSRF, robustes load_lessons
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
8b44279baa
commit
9c3d58b04f
4 changed files with 31 additions and 40 deletions
7
app.py
7
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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue