Feature: Periodischer Auto-Save (alle 10s, einstellbar) + Speichern bei Pause

Schließt die Lücke bei "hartem" Abbruch (Server-Stop/Absturz) ohne
vorheriges Speicher-Ereignis.

- main.js: zentrale Konstante AUTOSAVE_INTERVAL_SECONDS (Default 10);
  startAutoSave()/autoSave() speichern periodisch OHNE zu pausieren
  (überspringt bei Pause oder leerem Stand). togglePause() speichert
  zusätzlich beim Pausieren.
- session.js: flushElapsedTime() akkumuliert die laufende Zeit, ohne die
  Session zu pausieren -> korrekte Zeitwerte beim Auto-Save während
  aktivem Training.
- Resume-Route-Test ergänzt (update_progress -> /train stellt getippten
  Text + Cursor-Position wieder her). Testzahlen 96 -> 97.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-22 13:42:10 +02:00
commit 4a88414f82
6 changed files with 106 additions and 7 deletions

View file

@ -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

View file

@ -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

View file

@ -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();

View file

@ -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

View file

@ -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

View file

@ -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):