# REKORD - Technische Dokumentation Vollständige technische Doku für den selbstgehosteten Trainingsverlaufs-Tracker. Funktionen und Bedienung: siehe [README.md](README.md). - [Überblick & Stack](#überblick--stack) - [Projektstruktur](#projektstruktur) - [Zentrale Architektur-Entscheidungen](#zentrale-architektur-entscheidungen) - [Backend](#backend) - [HTTP-API (Referenz)](#http-api-referenz) - [Datenmodell (SQLite)](#datenmodell-sqlite) - [Set-Reconcile (Zustands-Isolierung)](#set-reconcile-zustands-isolierung) - [Einstellungen](#einstellungen) - [Auto-Reset / Autofill](#auto-reset--autofill) - [Export / Import](#export--import) - [Frontend](#frontend) - [Zustandsmodell & Kein-Re-Render-Prinzip](#zustandsmodell--kein-re-render-prinzip) - [Autosave](#autosave) - [Set-Kette & Abhak-Status](#set-kette--abhak-status) - [Theme & Akzent](#theme--akzent) - [config.json - Schema](#configjson---schema) - [Deployment](#deployment) - [Environment-Variablen](#environment-variablen) - [Docker / CasaOS](#docker--casaos) - [Tests](#tests) - [Erweitern](#erweitern) --- ## Überblick & Stack Ein Python-Backend aus der Standardbibliothek serviert statische Dateien und eine JSON-API; ein JS-Frontend ohne Framework baut die UI. Persistenz: SQLite in `data/state.db` (Pfad per `TRAINING_STATE_DIR`). ``` Browser (index.html, styles.css, app.js) | JSON-API (http.server) v server.py -> SQLite (sessions, entries, settings) | config.json (fachlicher Plan, serverseitig normalisiert) ``` Kein Framework, kein Build-Schritt, keine externen Abhängigkeiten (nur Python-Standardbibliothek). Portabel per `python3` oder Docker. ## Projektstruktur ``` server.py Backend: API, SQLite, Config-Normalisierung, Export/Import index.html Seitengerüst (Tabs, View-Container, Overlay, Toast) styles.css Design-Tokens + Komponenten (Theme/Akzent via data-Attribute) app.js Frontend: Zustandsmodell, Views, Autosave config.json Fachlicher Plan (Übungen, Sätze, Maschinen, Erklärungen) demo-data.json 5 Wochen Verlauf zum Ausprobieren (Import über Tab "Daten") tests/ Test-Suiten (API + jsdom-UI, offener Modus bzw. Auth) Dockerfile Container-Image (slim Python, nur Standardbibliothek) docker-compose.yml CasaOS-tauglich, Daten als echter Ordner data/ Zustand (SQLite), gitignored ``` ## Zentrale Architektur-Entscheidungen 1. **Konfiguration statt Code.** Der komplette Trainingsplan liegt in `config.json`. Der Server normalisiert die Rohstruktur beim Start einmal; das Frontend kennt nur die normalisierte Form. Ein Nicht-Programmierer pflegt nur die JSON. 2. **Stabile Set-IDs + Übungs-Isolierung.** Jedes Set ist eine DB-Zeile (`entries`) mit Auto-ID. Updates sind Upserts über `(session_id, exercise, sort)`. Löschen passiert nur innerhalb derselben Übung (`NOT IN` der gelieferten Sorts). Eine Änderung in Übung A berührt Übung B strukturell nie. 3. **Kein Re-Render beim Tippen.** Die Heute-Ansicht wird einmal gebaut; Tippen schreibt nur ins Set-State-Objekt und löst einen Debounce-Autosave aus. Das DOM wird nie global neu gebaut - Fokus/Cursor bleiben intakt. 4. **Keine Geister-Einträge.** `sessions.status` ist `active` oder `done`. Der Verlauf zeigt nur `done`. Gestartete Sessions überleben Reloads und sind per Knopf übertragbar oder verwerfbar. 5. **Serverzeit ist maßgeblich.** Datum und Endzeit kommen vom Server (`/api/meta`), damit nichts am Browser-TZ scheitert. 6. **Auto-Reset ist serverseitig und opt-in.** Nur wenn die Einstellung gesetzt ist, füllt der Server beim Session-Start das letzte Workout ein. 7. **Mehrbenutzer mit offenem Einzelmodus.** Solange kein Benutzer existiert, läuft die App ohne Anmeldung (user_id=0). Der erste registrierte Benutzer übernimmt vorhandene Daten; danach ist für alle Daten ein Bearer-Token nötig und alles ist strikt pro Benutzer getrennt. 8. **Design als Richtung, Nutzbarkeit zuerst.** 0px Radius, 2px-Borders, ein Akzent, dunkler Grund. Zustaende (Hover/Fokus/Aktiv) ueberall. Keine Verläufe, kein Glow, keine Emojis, keine Em-Dashes. --- ## Backend ### HTTP-API (Referenz) | Methode | Pfad | Beschreibung | |---|---|---| | GET | `/` | index.html | | GET | `/styles.css` | Styles | | GET | `/app.js` | Frontend-Code | | GET | `/api/meta` | `{today, now, auth_required, users, language}` - Serverzeit + Auth-Status + Server-Sprache | | GET | `/api/config` | normalisierte Konfiguration | | GET | `/api/settings` | Einstellungen des Benutzers (Default + gespeicherte Werte) | | PUT | `/api/settings` | Teil-Update, Body `{key: value}`, liefert vollständige Settings | | GET | `/api/sessions` | Filter: `activity`, `status` (`active`\|`done`), `from`, `to` | | POST | `/api/sessions` | Session starten, Body `{activity, day?, date?, start_time?}` | | PATCH | `/api/sessions/` | Teil-Update: `{meta: {...}}` und/oder `{sets: [...]}` | | POST | `/api/sessions//autofill` | "Alles zurücksetzen": Sätze auf letztes Workout setzen | | DELETE | `/api/sessions/` | Session samt Sätzen löschen | | GET | `/api/workouts` | abgeschlossene Workouts, Übungen gruppiert (Filter: `activity`, `from`, `to`, `limit`) | | GET | `/api/reset-values?exercise=&set=` | letzte abgeschlossene Messung eines Sets | | GET | `/api/stats?exercise=` | Steigerung je Termin (bestes Gewicht + Delta) | | GET | `/api/export?format=json\|csv` | kompletter Stand des Benutzers | | POST | `/api/import` | `{format, data}` (siehe Export/Import) | | POST | `/api/register` | erster Benutzer (nur solange keine existieren) | | POST | `/api/login` | `{username, password}` -> `{token, username, id}` | | POST | `/api/logout` | Token invalidieren | | POST | `/api/password` | eigenes Passwort ändern `{current_password, new_password}` (Auth) | | GET | `/api/me` | `{username, is_owner}` (401 ohne gültigen Token) | | GET | `/api/users` | Benutzerliste (nur Owner) | | POST | `/api/users` | Benutzer anlegen `{username, password}` (nur Owner) | | PUT | `/api/users//password` | Passwort setzen/zurücksetzen `{password}` (nur Owner, invalidiert alte Tokens) | | DELETE | `/api/users/` | Benutzer + Daten löschen (nur Owner, nicht selbst) | **PATCH `/api/sessions/`** akzeptiert zwei unabhängige Blöcke: ```json { "meta": { "date": "2026-09-20", "start_time": "17:30", "end_time": "18:40", "note": "...", "status": "done", "fields": { "distanceKm": "5,2" } }, "sets": [ { "exercise": "Push/Bankdrücken KH (je Hand)", "sort": 0, "weight": "18,5", "reps": "10", "done": 1 } ] } ``` - `meta.status` auf `done` ohne `end_time` trägt die Serverzeit ein. - `sets` wird pro Übung reconciliert (siehe unten). - Fehler: 400 (Body/Validierung), 404 (unbekannte Session), 500. ### Datenmodell (SQLite) ```sql CREATE TABLE sessions( id INTEGER PRIMARY KEY AUTOINCREMENT, activity TEXT NOT NULL, -- 'gym' | 'run' | 'ride' day TEXT NOT NULL DEFAULT '', -- Zyklus-Tag (Kraft), z.B. 'Push' date TEXT NOT NULL, -- YYYY-MM-DD (Serverzeit) start_time TEXT NOT NULL DEFAULT '', -- HH:MM end_time TEXT NOT NULL DEFAULT '', note TEXT NOT NULL DEFAULT '', fields TEXT NOT NULL DEFAULT '{}', -- Cardio-Felder als JSON status TEXT NOT NULL DEFAULT 'active', -- 'active' | 'done' created_at TEXT NOT NULL DEFAULT '', user_id INTEGER NOT NULL DEFAULT 0 -- 0 = offener Modus ); CREATE TABLE entries( id INTEGER PRIMARY KEY AUTOINCREMENT, -- stabile Set-ID session_id INTEGER NOT NULL, exercise TEXT NOT NULL, -- Schlüssel 'Tag/Übungsname' sort INTEGER NOT NULL DEFAULT 0, -- 0-basiert weight TEXT NOT NULL DEFAULT '', -- Rohstring, '' = leer (nie 0) reps TEXT NOT NULL DEFAULT '', done INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE settings( user_id INTEGER NOT NULL DEFAULT 0, -- 0 = offener Modus key TEXT NOT NULL, value TEXT NOT NULL, PRIMARY KEY(user_id, key) ); CREATE TABLE users( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, salt TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT '' ); CREATE TABLE tokens( token_hash TEXT PRIMARY KEY, -- SHA-256 des Bearer-Tokens user_id INTEGER NOT NULL, created_at TEXT NOT NULL DEFAULT '' ); ``` Gewichte/Reps werden als **Rohstrings** gespeichert ('' = leer, nie "0"), damit "leer eingeben = leer" gilt. Beide Dezimaltrennzeichen (`,` und `.`) werden beim Parsen verstanden. ### Set-Reconcile (Zustands-Isolierung) `_reconcile_sets(con, sid, sets)` gruppiert den Payload nach `exercise` und verarbeitet jede Übung getrennt: 1. Für jedes übermittelte Set: `UPSERT` über `(session_id, exercise, sort)`. Bestehende Zeilen behalten ihre ID. 2. Löschung nur von Sätzen **dieser Übung**, deren `sort` im Payload fehlt (`DELETE ... WHERE exercise=? AND sort NOT IN (...)`). Konsequenz: Der Client kann pro Übung einzeln speichern (nur "schmutzige" Übungen), ohne dass andere Übungen angefasst werden. Das `autofill`-Endpoint ist die einzige bulk-Ausnahme und bewusst explizit ("Alles zurücksetzen"). ### Einstellungen Defaults in `DEFAULT_SETTINGS`: ```python { "theme": "dark", # dark | system | light (system folgt dem Geraet) "oled": False, # reines Schwarz fuer den dunklen Modus "accent": "white", # Palette-Schluessel (siehe Frontend) "autoReset": False, # letztes Workout der Kategorie beim Session-Start laden "planPrefill": False, # Plan-Gewichte (startKg) beim Session-Start vorbefuellen "showTips": True, # Uebungs-Hinweise anzeigen "language": "de", # de | en "setupDone": False, # Ersteinrichtungs-Wizard abgeschlossen (serverweit) "onboarded": False, # persoenlicher Wizard (Sprache/Darstellung) abgeschlossen "weightUnit": "kg", # kg | lb (Anzeige-Einheit fuer Gewichte) "startTab": "today", # Standard-Ansicht beim Start } ``` `GET /api/settings` liefert immer den vollständigen, typ-korrekten Stand (Booleans als Boolean). Schichtung: Defaults < Server-Standard (user_id=0, gesetzt im Ersteinrichtungs-Wizard) < eigene Werte des Benutzers. Setzt der erste Benutzer (Owner) `language`/`accent`, wird das zugleich als Server-Standard übernommen. `GET /api/meta` enthält `language` (Server- Standard), damit auch der Login-Screen in der richtigen Sprache erscheint. Beim ersten registrierten Benutzer werden die Sessions aus dem offenen Modus übernommen (MOVE); die Settings werden KOPIERT, damit der Server-Standard für weitere Benutzer erhalten bleibt. ### Mehrbenutzer & Authentifizierung **Offener Modus:** Solange die `users`-Tabelle leer ist, ist `user_id = 0` (OPEN_USER) und alle `/api/*`-Endpunkte funktionieren ohne Token - exakt wie im Einzelmodus. Beim ersten registrierten Benutzer (uid 1 = Owner) werden vorhandene Sessions/Einstellungen per Migration übernommen (`UPDATE ... WHERE user_id = 0`). **Auth:** Sobald ein Benutzer existiert, liefert `_require_user()` 401, wenn kein gültiges `Authorization: Bearer ` mitkommt. Öffentlich bleiben nur `/api/meta`, `/api/login`, `/api/register` sowie die statischen Dateien. - Passwörter: PBKDF2-HMAC-SHA256, 200.000 Iterationen, Salt pro Benutzer. - Tokens: 32-Byte-Zufall, nur der SHA-256-Hash liegt in der DB (`tokens.token_hash`). - Passwort ändern: `POST /api/password` (eigenes, mit aktuellem Passwort), `PUT /api/users//password` (Owner-Reset, invalidiert alte Tokens des Benutzers). - Rechte: Benutzerverwaltung (`GET/POST/PUT/DELETE /api/users`) nur für den Owner (uid 1). Löschen entfernt Sessions, Entries, Settings und Tokens. **Daten-Scoping:** Alle Datenabfragen laufen über `_user_clause(user_id)` (`user_id = ?`), im offenen Modus `user_id = 0`. Sessions, Settings, Reset- Werte, Statistiken, Export/Import und die Workouts-API sind damit strikt pro Benutzer getrennt. ### Workouts-API `GET /api/workouts` liefert abgeschlossene Sessions in einer für externe Auswertungen gedachten Form: ```json [ { "id": 5, "activity": "gym", "activity_name": "Gym (Kraft)", "day": "Push", "date": "2026-09-20", "start_time": "18:00", "end_time": "19:15", "duration_min": 75, "note": "", "exercises": [ { "exercise": "Push/Bankdrücken KH (je Hand)", "name": "Bankdrücken KH (je Hand)", "sets": [ { "sort": 0, "weight": "18,5", "reps": "10", "done": 1 } ] } ], "fields": {} } ] ``` Filter: `activity`, `from`, `to`, `limit`. Cardio-Workouts haben leere `exercises` und die Feldwerte in `fields`. ### Auto-Reset / Autofill `last_workout_sets(exercise, exclude_session)`: - Findet die jüngste abgeschlossene Session (`status='done'`, ungleich der aktuellen in-Arbeit-Session) mit mindestens einem Satz, der ein Gewicht hat. - Liefert nur Sätze mit `weight != ''`, in Original-Reihenfolge. `autofill_session(sid, exercises=None)`: - `exercises=None` → alle Übungen des Sessions-Tags (aus der Config) plus alle, die bereits Sätze haben. - Pro Übung: alte Sätze löschen, gefüllte Sätze des letzten Workouts neu einfügen, `done = 0` (frische Abhak-Status). - Übungen **ohne** Verlauf bleiben unangetastet. Aufrufstellen: - `POST /api/sessions` - nur wenn `settings.autoReset` aktiv und die Aktivität `strength` ist. - `POST /api/sessions//autofill` - manuell über den Knopf "Alles zurücksetzen" im Session-Kopf. ### Export / Import - **JSON** (`trainingsplan-export-v2`): `{format, exported_at, config, sessions}`. Import = Merge per Session-ID: existiert die ID im eigenen Konto, wird die Session aktualisiert (Entries ersetzt), sonst neu angelegt - die Original-ID wird übernommen, wenn sie global frei ist; belegte IDs (z.B. durch einen anderen Benutzer) bekommen eine freie ID oberhalb aller bekannten. Nichts wird gelöscht, pro Benutzer getrennt. - **CSV**: eine Zeile pro Kraft-Satz (`kind=set`) bzw. pro Cardio-Session (`kind=cardio`). Spalten: `session_id, activity, day, date, start_time, end_time, note, status, kind, exercise, set, weight, reps, done` plus die Cardio-Feld-Schlüssel aus der Config. Import gruppiert per `session_id` (oder per Session-Tupel bei handgebauten Dateien). BOM wird beim Import entfernt. --- ## Frontend ### Zustandsmodell & Kein-Re-Render-Prinzip Ein `SessionModel` (`makeModel`) hält pro Aktivität: - `exercises[exerciseKey]` - Array von Set-Objekten `{id, weight, reps, done}` - `exKeys` - geordnete Liste der Übungen dieser Session (Tag + manuell hinzugefügte aus anderen Tagen) - `dirtyEx` - Set der Übungen mit ungespeicherten Änderungen - `metaDirty` - Notiz/Cardio-Felder geändert Die Heute-Ansicht wird **einmal** gebaut. Jede Eingabe schreibt nur in das eigene Set-Objekt und markiert die Übung als dirty. Es gibt keine globalen Re-Renders; lokale Rebuilds passieren nur bei expliziten Aktionen (Satz hinzufügen/entfernen, Übung hinzufügen/entfernen, "Alles zurücksetzen") - nie während des Tippens. ### Autosave `scheduleAutosave` debounced (700 ms) → `flushAutosave`: - sendet **nur** dirty Übungen als `PATCH {sets}` (pro Übung) und dirty Meta als `PATCH {meta}`. - Entfernte Übungen werden mit leerem Set-Array übertragen → der Server löscht ihre Einträge. - Ein globaler Busy-Flag mit Queue-Activity verhindert parallele Flushes mehrerer Aktivitäten. - `finishSession` flusht zuerst, bricht bei nicht gespeicherten Änderungen ab, setzt dann `status=done` + `end_time` und baut die Karte zurück auf "Session starten". ### Set-Kette & Abhak-Status `toggleSetDone` hält die Invariante "aktiv ist ein Präfix": - Klick auf Set *i* (nicht aktiv) → Sets 1..i aktiv. - Klick auf Set *i* (aktiv) → Sets i..n inaktiv (rückwärts). - Gewicht/Reps werden nie verändert; `aria-pressed` und die Ankreuz-Box (`✓`) werden mitgeführt. Der Abhak-Status wird in `entries.done` gespeichert und im Verlauf angezeigt (✓/□ je Satz) sowie im Inline-Editor als Checkbox bearbeitet. ### Übung aus anderem Tag Der Picker im Session-Kopf listet alle Übungen des Zyklus außer den bereits enthaltenen. `addExercise` hängt die Übung an `exKeys` und rendert den Block; `removeExercise` (nur für nicht-Tag-Übungen) entfernt ihn und meldet die Übung als dirty (leeres Set-Array → Server löscht). Da Übungen erst mit Werten persistieren, entstehen keine Geister-Einträge. ### Theme & Akzent `applySettings` löst das Theme auf: `system` wird über `prefers-color-scheme` auf dark/light abgebildet (mit Media-Query-Watch); ist `oled` aktiv und die Auflösung dark, greifen die rein-schwarzen Tokens. Danach setzt es `data-theme` und `data-accent` auf ``. Die Design-Tokens liegen als CSS-Variablen in `:root`; Theme- und Akzent-Overrides stehen in `html[data-theme=...]` / `html[data-accent=...]`. Alle Akzente nutzen eine einheitliche dunkle Tinte (`--accent-ink`) mit dedizierten Hover-/Active-Tönen (mindestens 4,2:1 Kontrast). `showTips` steuert zusätzlich die Klasse `no-tips` auf der Heute-Ansicht. ### Sprache (i18n) Alle UI-Texte laufen über `t(key)` aus dem `LANG`-Dictionary (de/en). `lang()` liest `app.settings.language`; ohne Einstellung fällt es auf `de` zurück. Häufige Server-Fehlermeldungen werden über `eMsg()` lokalisiert (z.B. "Benutzername oder Kennwort inkorrekt"). Der Login-Screen nutzt die Server-Standardsprache aus `GET /api/meta` (settings werden erst nach Login geladen). ### Ersteinrichtungs-Wizard & Onboarding - **Wizard** (`renderSetupWizard`): einmalig, solange `setupDone` fehlt. Schritte mit expliziter Navigation (Zurück / Weiter / Einrichtung abschließen), Auswahl springt NICHT automatisch weiter; eine Segment-Fortschrittsleiste (`wizardProgress`) oben zeigt den Stand: 1. Sprache (serverweit) - kommt zuerst, damit der Wizard lesbar ist 2. Einzel- oder Mehrbenutzermodus (bei Mehrbenutzer wird der erste Benutzer direkt angelegt) 3. Darstellung: Theme (Dunkel/System/Hell) + OLED-Schalter + Akzentfarbe Abschließen persistiert alles und setzt `setupDone` (+ `onboarded` für den Owner). - **Onboarding**: einloggende Benutzer ohne `onboarded` durchlaufen einmalig Sprache -> Darstellung für ihr eigenes Konto. ### Session-Vorbefuellung beim Start `fill_session_start` befüllt eine frische Session pro Übung: - `autoReset` (Letztes Training vorbefüllen): letztes abgeschlossenes Workout der Übung (nur gefüllte Sätze) - hat Vorrang. - `planPrefill` (Plan-Gewichte vorbefüllen): startKg aus der Config, Anzahl wie im Plan, auch ohne Verlauf. Übungen ohne Quelle bleiben leer (der Client seedet Plan-Sätze leer). ### Uebungen sortieren & Wochen-Uebersicht - **Sortieren**: Drag & Drop auf dem Uebungs-Block (`attachDragReorder`); die Reihenfolge wird in `session.fields.exerciseOrder` gespeichert und bei `makeModel` wiederhergestellt. - **Wochen-Uebersicht**: im Verlauf zwischen Liste und Woche umschaltbar; eine 7-Tage-Spalte (Montag-Sonntag) mit den Sessions des Tages, blätterbar über Vorige/Nächste Woche. --- ## config.json - Schema ```jsonc { "profile": { "name": "", "weightUnit": "kg", "repsLabel": "reps" }, "activities": [ { "id": "gym", // eindeutig "name": "Gym (Kraft)", "type": "strength", // strength | cardio "active": true, "displayOrder": 1, "cycle": { // nur strength "name": "Push/Pull/Legs", "days": [ { "key": "Push", // Teil des Übungs-Schlüssels "title": "Push", "exercises": [ { "name": "Bankdrücken KH (je Hand)", "sets": 4, // Zahl = uniform ODER Array für einzelne Sätze: // "sets": [ { "weight": 18, "reps": 8 }, { "weight": 15, "reps": 10 } ], "reps": "8-10", // Ziel, als Hinweis "startKg": 18, // Start-Gewicht, als Hinweis "tags": ["brust", "kh"], "info": "Erklärung + passende Maschine (Fragezeichen-Overlay)" } ] } ] } }, { "id": "run", // Cardio "name": "Laufen", "type": "cardio", "displayOrder": 2, "fieldTemplates": [ { "key": "distanceKm", "label": "Distanz", "unit": "km", "kind": "number", "step": 0.1 }, { "key": "minutes", "label": "Dauer", "unit": "min", "kind": "number" }, { "key": "pacePerKm", "label": "Pace", "unit": "min/km", "kind": "pace" }, { "key": "heartRate", "label": "Puls Ø", "unit": "bpm", "kind": "number", "optional": true }, { "key": "elevation", "label": "Höhenmeter", "unit": "m", "kind": "number", "optional": true } ] } ] } ``` Der Server normalisiert die Struktur einmalig beim Start (`normalize_config`). Verlaufsdaten hängen an den Übungsschlüsseln `Tag/Übungsname` - **Übungen umbenennen bricht die Zuordnung zum alten Verlauf**. --- ## Deployment ### Environment-Variablen | Variable | Default | Bedeutung | |---|---|---| | `PORT` | `8080` | HTTP-Port | | `TRAINING_STATE_DIR` | `./data` | Verzeichnis für `state.db` | Port auch als erstes Argument: `python3 server.py 8080`. ### Docker / CasaOS - `Dockerfile`: `python:3.13-slim`, `WORKDIR /app`, `VOLUME /app/data`, `HEALTHCHECK` gegen `/api/meta`. - `docker-compose.yml`: bindet `/DATA/AppData/trainingsplan/data` als echten Ordner (CasaOS-tauglich, sichtbar/backupbar). Optional kann `config.json` als read-only Mount eingebunden werden, um den Plan ohne Image-Rebuild zu ändern (danach Container neu starten). --- ## Tests Vier Testläufe unter `tests/` (laufen gegen einen echten Server-Subprozess mit frischem `TRAINING_STATE_DIR`): | Suite | Checks | Inhalt | |---|---|---| | `test_api.py` | 39 | Session-Lebenszyklus, Set-Isolierung, stabile IDs, Reset, Statistik, Export/Import, Fehlerfälle (offener Modus) | | `test_api_settings.py` | 18 | Settings-Defaults/-Persistenz/-Typen, Auto-Reset (nur gefüllte Sets), Autofill, Plan-Gewichte vorbefüllen (mit/ohne Verlauf) | | `test_auth.py` | 43 | Register/Login/Logout, 401-Gate, Alt-Daten-Migration, Benutzerverwaltung, Passwort ändern/zurücksetzen, Server-Sprach-Standard, strikte Daten-Isolation, Workouts-API, Demo-Import, Benutzer-Löschung | | `test_ui.js` (jsdom) | 28 | Ersteinrichtungs-Wizard (Zurück/Weiter), Session starten, Tippen ohne Fokus-Verlust, Set-Button/Box, Set-Kette, Übung aus anderem Tag, Drag & Drop sortieren, Wochenübersicht, Theme/System/OLED/Akzent, Toggles, Auto-Reset-Flow | | `test_ui_auth.js` (jsdom) | 23 | Wizard (Modus, Sprache, Darstellung), Sprachwechsel live, Onboarding, Login-Gate mit Inline-Fehler, eingeloggter Start, Einstellungen (Konto-Kennwort, Admin nicht in Benutzerliste) | `test_ui.js` benötigt jsdom: `npm install jsdom@22.1.0` und Start mit `NODE_PATH=<...>/node_modules node tests/test_ui.js`. --- ## Erweitern - **Neue Übung/neuen Tag:** in `config.json` ergänzen (siehe Schema). Server neu starten. - **Neues Cardio-Feld:** `fieldTemplates` erweitern; Unit/Label kommen aus der Config, der Export/Import baut die CSV-Spalten automatisch daraus. - **Neue Einstellung:** `DEFAULT_SETTINGS` in `server.py` + Eintrag in `renderSettingsView` in `app.js`. - **Neuer Theme-/Akzentwert:** CSS-Override in `styles.css` ergänzen; den Akzent zusätzlich in `ACCENTS` (app.js) für die Swatches registrieren. - **Neue API-Route:** Handler in `server.py` unter `do_GET`/`do_POST`/ `do_PATCH`/`do_PUT`/`do_DELETE` ergänzen.