Startpunkt 02.10.2026: aktueller Stand des Trainingsplans

Historie bewusst neu aufgesetzt, ein einziger Commit als neuer Ausgangspunkt.
This commit is contained in:
AI-Agent committed 2026-10-02 01:23:25 +02:00
commit 8fc77212cb
18 files changed
+10863

No files matched your search

+538
View File
@@ -0,0 +1,538 @@
# 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/<id>` | Teil-Update: `{meta: {...}}` und/oder `{sets: [...]}` |
| POST | `/api/sessions/<id>/autofill` | "Alles zurücksetzen": Sätze auf letztes Workout setzen |
| DELETE | `/api/sessions/<id>` | 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/<id>/password` | Passwort setzen/zurücksetzen `{password}` (nur Owner, invalidiert alte Tokens) |
| DELETE | `/api/users/<id>` | Benutzer + Daten löschen (nur Owner, nicht selbst) |
**PATCH `/api/sessions/<id>`** 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 <token>` 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/<id>/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/<id>/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 `<html>`. 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.