diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md index 4bbbc41..1bc75fa 100644 --- a/PROJECT_STRUCTURE.md +++ b/PROJECT_STRUCTURE.md @@ -97,7 +97,7 @@ typewriter/ - `setup_database.py` - Datenbank-Setup ### 🔴 Tests (versioniert) -- `tests/` - Unit-, Integration- & Route-Tests (96 Tests) +- `tests/` - Unit-, Integration- & Route-Tests (97 Tests) ### ⚫ Temporär/Generiert (NICHT versioniert) - `venv/` - Virtual Environment diff --git a/README.md b/README.md index 16d8f28..0b7a826 100644 --- a/README.md +++ b/README.md @@ -204,16 +204,16 @@ Die Statistik-Seite (`/statistics`) bietet: ## 🧪 Tests -Das Projekt enthält eine umfassende Test-Suite mit **96 Tests** (100% Erfolgsrate) und lokaler Validierung per Pre-commit-Hook (ruff + Tests). +Das Projekt enthält eine umfassende Test-Suite mit **97 Tests** (100% Erfolgsrate) und lokaler Validierung per Pre-commit-Hook (ruff + Tests). ### Test-Übersicht | Kategorie | Anzahl | Status | |-----------|--------|--------| -| **Gesamt** | 96 Tests | ✅ 100% | +| **Gesamt** | 97 Tests | ✅ 100% | | **Unit Tests** | 54 Tests | ✅ Bestanden | | **Integration Tests** | 31 Tests | ✅ Bestanden | -| **Route Tests** | 11 Tests | ✅ Bestanden | +| **Route Tests** | 12 Tests | ✅ Bestanden | ### Test-Ausführung diff --git a/static/js/main.js b/static/js/main.js index 432ce7e..d9f3b34 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -11,6 +11,9 @@ import { KeyboardHandler } from './keyboard.js'; import { SessionManager } from './session.js'; import { UIManager } from './ui.js'; +// Auto-Save: Intervall in Sekunden. Zentraler Konfigwert – hier anpassbar. +const AUTOSAVE_INTERVAL_SECONDS = 10; + class TypewriterApp { constructor() { // Application state @@ -36,6 +39,8 @@ class TypewriterApp { // Time display update interval this.timeDisplayInterval = null; + // Auto-Save interval + this.autoSaveInterval = null; } /** @@ -76,6 +81,9 @@ class TypewriterApp { // Start time display update interval (updates every second) this.startTimeDisplayUpdate(); + // Start periodic auto-save + this.startAutoSave(); + console.log('Typewriter Tutor initialized successfully'); } @@ -372,6 +380,53 @@ class TypewriterApp { } } + /** + * Start the periodic auto-save interval. + * Speichert den Fortschritt regelmäßig (alle AUTOSAVE_INTERVAL_SECONDS), + * solange aktiv trainiert wird. + */ + startAutoSave() { + if (this.autoSaveInterval) { + clearInterval(this.autoSaveInterval); + } + this.autoSaveInterval = setInterval( + () => this.autoSave(), + AUTOSAVE_INTERVAL_SECONDS * 1000 + ); + } + + /** + * Stop the periodic auto-save interval. + */ + stopAutoSave() { + if (this.autoSaveInterval) { + clearInterval(this.autoSaveInterval); + this.autoSaveInterval = null; + } + } + + /** + * Periodischer Auto-Save: speichert den aktuellen Stand, OHNE das + * Training zu unterbrechen. Überspringt, wenn pausiert oder noch nichts + * getippt wurde. + */ + async autoSave() { + if (this.sessionManager.isPaused) return; + if (this.currentCharIndex === 0 && this.userInput === '') return; + + // Verstrichene Zeit aktualisieren, ohne zu pausieren + this.sessionManager.flushElapsedTime(); + try { + await this.sessionManager.saveProgress( + this.userInput, + this.currentCharIndex, + this.fullText + ); + } catch (error) { + console.error('Auto-Save fehlgeschlagen:', error); + } + } + /** * Update progress bar based on current position in lesson */ @@ -451,6 +506,13 @@ class TypewriterApp { if (this.sessionManager.isPaused) { this.metronomePlayer.stop(); + // Beim Pausieren den aktuellen Stand speichern (Zeit ist durch + // togglePause bereits geflusht). + this.sessionManager.saveProgress( + this.userInput, + this.currentCharIndex, + this.fullText + ).catch(error => console.error('Speichern bei Pause fehlgeschlagen:', error)); } else { if (this.metronomePlayer.enabled) { this.metronomePlayer.start(); diff --git a/static/js/session.js b/static/js/session.js index b5981a7..2f7b703 100644 --- a/static/js/session.js +++ b/static/js/session.js @@ -83,6 +83,24 @@ export class SessionManager { this.lastStartTime = new Date(); } + /** + * Akkumuliert die bisher verstrichene Zeit in totalElapsedTime / + * netTrainingTime, OHNE die Session zu pausieren. Wird vom periodischen + * Auto-Save genutzt, damit der gespeicherte Zeitwert aktuell ist, während + * das Training ununterbrochen weiterläuft. + */ + flushElapsedTime() { + if (this.lastStartTime !== null) { + const now = new Date(); + const delta = now - this.lastStartTime; + this.totalElapsedTime += delta; + if (!this.isPaused) { + this.netTrainingTime += delta; + } + this.lastStartTime = now; // Zeitmessung läuft weiter + } + } + /** * Toggle pause state * @returns {boolean} New pause state diff --git a/tests/README.md b/tests/README.md index d2b2bab..cac3352 100644 --- a/tests/README.md +++ b/tests/README.md @@ -27,13 +27,13 @@ Dieses Verzeichnis enthält Unit Tests für die Service-Layer der Typewriter Tra | Bereich | Test-Datei | Anzahl Tests | Status | |---------|-----------|--------------|--------| -| **Routen & API** | `test_routes.py` | 11 | ✅ Alle bestanden | +| **Routen & API** | `test_routes.py` | 12 | ✅ Alle bestanden | -**Gesamt Route Tests:** 11 Tests, alle bestanden ✅ +**Gesamt Route Tests:** 12 Tests, alle bestanden ✅ ### Gesamtübersicht -**Gesamt:** 96 Tests (54 Unit + 31 Integration + 11 Route), **alle bestanden** ✅ +**Gesamt:** 97 Tests (54 Unit + 31 Integration + 12 Route), **alle bestanden** ✅ ### Hinweise diff --git a/tests/test_routes.py b/tests/test_routes.py index b22bf19..c7d1238 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -102,6 +102,25 @@ class TestTrainingApi(RouteTestBase): }) self.assertEqual(r.status_code, 400) + def test_progress_persists_and_restores(self): + # Teilfortschritt speichern (so wie es der Auto-Save / beforeunload tut) + r = self.client.post('/update_progress', json={ + 'user_input': 'ffff jj', + 'current_text': 'ffff jjjj', + 'elapsed_time': 8.0, + 'cursor_position': 7, + 'total_elapsed_time': 8000, + 'net_training_time': 8000, + 'key_stroke_count': 7, + 'is_paused': False, + }) + self.assertEqual(r.status_code, 200) + + # /train erneut laden -> Stand (getippter Text + Position) wiederhergestellt + html = self.client.get('/train').get_data(as_text=True) + self.assertIn('window.lastText = "ffff jj"', html) + self.assertIn('window.currentCharIndex = 7', html) + class TestStatisticsApi(RouteTestBase):