Files
trainingsplan/TECH.md
T
AI-Agent 8fc77212cb Startpunkt 02.10.2026: aktueller Stand des Trainingsplans
Historie bewusst neu aufgesetzt, ein einziger Commit als neuer Ausgangspunkt.
2026-10-02 01:23:25 +02:00

23 KiB

REKORD - Technische Dokumentation

Vollständige technische Doku für den selbstgehosteten Trainingsverlaufs-Tracker. Funktionen und Bedienung: siehe README.md.


Ü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:

{ "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)

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:

{
    "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:

[ { "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

{
  "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.