Feature #77
Von Julius Haman vor 6 Tagen aktualisiert
Ziel: Auf der Trainingsdetailseite aus der Verteilung der Pulszonen zwei einfache Angaben ableiten - **Belastung** (wie anstrengend war das Training) und **Schwerpunkt** (wofür war es hauptsächlich gut). Bewusst nicht "Nutzen des Trainings": Die Zonenverteilung beschreibt die Dosis, nicht die Wirkung, denn die hängt von Ziel, Trainingszustand und Erholung ab.
### Ausgangslage
* `TrainingDetailViewModel.BuildZonePie` berechnet die Zeit je Pulszone (`zoneSeconds[5]`, zeitgewichtet über die Zeitabstände der Trainingspunkte) bereits, nutzt sie aber nur für das Kreisdiagramm.
* Zonenmodell (`User.PulseZoneIndex`): 5 gleich breite Zonen mit 50/60/70/80/90/100 % von HFmax (Erholung, Leicht, Mittel, Hart, Maximum). HFmax historisiert über `Training.MaxPulseAtRecording`, Fallback auf den aktuellen Benutzer.
* `PulseZoneIndex` klemmt Werte unter 50 % auf Zone 1 - für die Belastung muss Puls unter 50 % HFmax separat behandelt werden (Gewicht 0).
* Im Benutzermodell gibt es keine FTP/Schwellenleistung (`StartingPower` ist nur der Startwert der pulsgesteuerten Regelung). Ein leistungsbasierter Wert ist daher nicht Teil dieses Tickets.
* `FitnessNote` ist bereits durch die Erholungspuls-Note belegt und wird nicht wiederverwendet.
### Belastung (Edwards-TRIMP)
Belastung = Summe über alle Zonen von (Minuten in der Zone x Zonennummer). Erholung = 1, Leicht = 2, Mittel = 3, Hart = 4, Maximum = 5 Punkte pro Minute. Puls unter 50 % HFmax zählt 0.
* Ergebnis ist eine Zahl ohne Einheit, nur zum Vergleichen (240 ist etwa doppelt so belastend wie 120).
* Beispiel: 10 min Erholung + 30 min Leicht + 15 min Mittel + 5 min Hart = 10 + 60 + 45 + 20 = 135 Punkte.
* Lange lockere Trainings bekommen dadurch automatisch viele Punkte (2 h Leicht = 240 = 1 h Hart). Das ist gewollt.
* Optional später: Einordnung relativ zur eigenen Historie ("leichter / üblich / härter als die letzten 10 Trainings"), statt absoluter Schwellen.
### Schwerpunkt (regelbasiertes Label)
Wird ausschließlich aus der prozentualen Zeitverteilung berechnet, unabhängig von der Belastung. Regeln der Reihe nach geprüft:
| Bedingung (Zeitanteil) | Label |
|---|---|
| Dauer unter 10 min | kein Label |
| Zone 1 >= 60 % | Erholung |
| Zone 5 >= 10 % oder (Zone 4 + Zone 5) >= 30 % mit deutlichem Wechsel zwischen hoch und niedrig | Intervall / hochintensiv |
| Zone 4 >= 40 % | Schwelle / Tempo |
| Zone 2 + Zone 3 >= 70 % | Grundlagenausdauer |
| sonst | gemischt |
* Schwellenwerte als benannte Konstanten, damit sie sich leicht nachjustieren lassen. Es sind Heuristiken.
* Grenze: Der Puls hinkt bei kurzen Intervallen hinterher, "Intervall" wird dort eher zu selten erkannt.
* Die Zonen laufen nach % von HFmax; Zone 3 entspricht damit etwa dem klassischen GA1, Zone 4 dem Schwellenbereich. Die 3-Zonen-Aufteilung nach Seiler passt nicht 1:1.
### Umsetzung
1. **Rechenklasse** in ErControl.Core (z. B. `TrainingLoadCalculator`): liefert Sekunden je Zone, Belastung und Schwerpunkt. `BuildZonePie` nutzt sie ebenfalls (Logik nur an einer Stelle), gut testbar.
2. **Trainingsdetailseite**: Kachel "Belastung" und Zeile "Schwerpunkt" beim Kreisdiagramm. Texte in die Resx unter `TrainingSessionsViewModel.*`. Wird beim Öffnen berechnet, keine Migration nötig.
### Umgesetzt
* **Rechenklasse `TrainingLoadCalculator`** (ErControl.Core/Services): liefert Sekunden je Pulszone, Belastung (Edwards-TRIMP) und Schwerpunkt. Die Schwellenwerte der Schwerpunkt-Regeln stehen als Konstanten dort. Der "deutliche Wechsel zwischen hoch und niedrig" ist als Zone 4+5 >= 30 % **und** Zone 1+2 >= 20 % umgesetzt. Mit Unit-Tests (`TrainingLoadCalculatorTests`)
* **Trainingsdetailseite**: Belastung und Schwerpunkt stehen unterhalb der Legende in der Pulszonen-Karte (keine Kachel). Die Zeile Schwerpunkt entfällt, wenn das Training keinen bekommt (unter 10 min). Texte in der Resx (Deutsch/Englisch) unter `TrainingSessionsViewModel.*`; berechnet beim Öffnen der Seite, keine Migration
* **Kreisdiagramm** nutzt dieselbe Auswertung. Änderung: Trainingspunkte ohne Pulsmessung (Puls 0) zählen nicht mehr als Zone Erholung - ein Training ohne Brustgurt zeigt kein Pulszonen-Diagramm mehr statt 100 % Erholung
### Später (nicht Teil dieses Tickets)
* Spalten "Belastung" und "Schwerpunkt" in der Trainingsübersicht (Standard ausgeblendet). Dafür Wert speichern (neue Spalte, Migration, einmaliger Backfill wie bei `MaxPulseAtRecording`), sonst müsste das Grid für jedes Training alle `TrainingPoints` laden.
* Wochensumme der Belastung.
* Leistungsbasierte Belastung (TSS-artig), sobald ein FTP-Feld im Benutzer existiert.
* Banister-TRIMP mit Ruhepuls (`RestingPulseAtRecording` ist bereits historisiert) als Alternative zu Edwards.
### Offene Entscheidungen
* Grid-Spalten: umgesetzt wird das in #78. Umsetzung zunächst nur Detailseite (Schritt 1 und 2) oder gleich mit Grid-Spalten?
* Schwellenwerte und Bezeichnungen der Labels bestätigen.