# Server-to-Server Sync: CyccessWeb ↔ TrainingsplanServer

## Kontext

CyccessWeb und TrainingsplanServer synchronisieren beide mit dem MAT, aber nicht miteinander. Daten von Cardio-Geräten (TrainingsplanClient → TrainingsplanServer) erreichen CyccessWeb nicht, und vom Trainer geplante Einheiten (MAT → CyccessWeb) erreichen den TrainingsplanServer nicht. Wir fügen direkten Server-to-Server-Sync hinzu.

---

## Authentifizierung

Shared API Key im `X-Server-ApiKey` Header. Einfacher als JWT für Server-zu-Server-Kommunikation.

**TrainingsplanServer appsettings:**
```json
"ServerSync": {
  "ApiKey": "...",
  "CyccessWebUrl": "https://..."
}
```

**CyccessWeb appsettings:**
```json
"ServerSync": {
  "ApiKey": "...",
  "TrainingsplanServerUrl": "https://..."
}
```

Validierung über ein `ServerApiKeyAttribute` (IAuthorizationFilter), das `X-Server-ApiKey` gegen die konfigurierte `ServerSync:ApiKey` prüft. Wenn kein ApiKey konfiguriert → 503 (Sync nicht aktiv). Wenn falscher Key → 401.

---

## Shared DTO

CyccessWeb bekommt eine Projektreferenz auf `TrainingsplanServer.Interface` (enthält nur `JsonTrainingseinheit`, `JsonUbung`, `JsonTrainingsplan`, `JsonMessage` — keine weiteren Abhängigkeiten).

Neues DTO in `TrainingsplanServer.Interface`:
```csharp
public class ServerSyncRequest
{
    public Guid AthleteId { get; set; }
    public int Sender { get; set; }  // 0=Trainer, 1=Cardio, 2=Copy
    public JsonTrainingseinheit Trainingseinheit { get; set; }
}
```

---

## Richtung 1: TrainingsplanServer → CyccessWeb

### Auslöser
Nach `ImportController.Cardio` und `ImportController.Copy`, wenn `SaveChanges()` erfolgreich war.

### Neuer Endpoint auf CyccessWeb
`POST /api/serversync/trainingseinheit` — `ServerSyncController.cs`

Logik:
1. Finde `ApplicationUser` wo `AthleteID == request.AthleteId`
2. Kein User gefunden → `200 OK` (Athlet unbekannt, verwerfen)
3. Prüfe ob TE mit dieser ID schon existiert → `200 OK` (Duplikat, skip)
4. **Sender==1 (Cardio):** Für jede JsonUbung:
   - Erstelle `EnduranceExercise` mit `Duration = TimeSpan.FromMinutes(Minutes)`
   - Suche `LoadZone` wo `PrimaryMin == MinimumHeartRate && PrimaryMax == MaximumHeartRate`
   - Falls keine gefunden → `LoadZoneId = null`
5. **Sender==2 (Copy):**
   - Suche Template-Session mit `Id == CopiedFromID`
   - Falls nicht gefunden → Suche per Name
   - Kopiere Übungen vom Template, überschreibe Sets/Reps/Load aus JsonUbung-Daten
6. Erstelle `TrainingSession` + `TrainingAppointment` (Zeit aus `JsonTrainingseinheit.Time`)
7. `SaveChanges()`, Kalender-Notification senden

*Orientiert sich an `AthletTrainingMainModel.DownloadTrainingseinheiten` (Zeile 2115-2209)*

### Outbound-Service auf TrainingsplanServer
`CyccessWebSyncService` — Typed HttpClient mit `X-Server-ApiKey` Header.

In `ImportController.Cardio` und `Copy`:
```csharp
// Nach _db.SaveChanges():
await _cyccessWebSync.ForwardTrainingseinheitAsync(id, trainingseinheit, sender: 1 oder 2);
```
Fehler werden geloggt, aber die Originalantwort bleibt Ok(). `ImportController` wird dafür `async`.

---

## Richtung 2: CyccessWeb → TrainingsplanServer

### Auslöser
Nach:
- `CreateAppointment` (Type==1 = TrainingAppointment, DataId vorhanden)
- `UpdateAppointment` (Type==1, DataId vorhanden)
- `UpdateSession` (zugehörigen Appointment suchen für die Zeit)

**Nicht** nach `CreateSession` (noch kein Appointment mit Zeitinfo vorhanden).

### Neuer Endpoint auf TrainingsplanServer
`POST /api/serversync/trainingseinheit` — `ServerSyncController.cs`

Logik:
1. Prüfe ob Athlet mindestens eine `DatabaseTrainingseinheit` hat → wenn nicht, `200 OK` (verwerfen)
2. Falls TE mit gleicher ID existiert: Update (nur wenn `Sender==0`)
3. Sonst: Insert mit `Sender=0`

### Outbound-Service auf CyccessWeb
`TrainingsplanServerSyncService` — Typed HttpClient.

Konvertiert CyccessWeb `TrainingSession` + `Exercises` → `JsonTrainingseinheit` + `JsonUbung[]`:

| CyccessWeb Exercise | → JsonUbung |
|---------------------|-------------|
| `EnduranceExercise` | `Minutes` = Duration.TotalMinutes, `MinHR`/`MaxHR` aus LoadZone.PrimaryMin/Max |
| `CyclingExercise` | `Minutes` = Duration.TotalMinutes |
| `StrengthExercise` | `Sets`, `Repetitions`, `Load`, Pausen-Felder |
| `StrengthEnduranceExercise` | wie Strength |
| `BodybasicsExercise` | `Sets`, `Repetitions` |
| `SpeedExercise` | `Sets`=Series, `SecondsForOneRepetition`=Seconds |
| `SkiJumpExercise` | `Sets`=Jumps |
| `AlpineSkiExercise` | `Sets`=Runs, `SecondsForOneRepetition`=Seconds |
| `KayakExercise` | `Sets`=Series, `SecondsForOneRepetition`=Seconds |
| Game/Tennis/Badminton | `Minutes` = Duration.TotalMinutes |

*Orientiert sich an `AthletTrainingMainModel.CreateJsonÜbung` (Zeile 2231-2290) und `TrainingController.MapExerciseToDto` (Zeile 451-533)*

Für die `JsonTrainingseinheit`:
- `ID` = Session.Id
- `Name` = Session.Name
- `Time` = Appointment.StartTime
- `Color` = LoadZone.Color der ersten Übung (oder `0xff000000` als Default)

In `TrainingController`:
```csharp
// Nach CreateAppointment/UpdateAppointment (Type==1, DataId vorhanden):
await _tpServerSync.ForwardSessionAsync(athleteId, session, appointmentTime);

// Nach UpdateSession:
// Appointment(s) zu dieser Session suchen, für jeden forwarden
```

---

## Loop-Prevention

Kein Risiko von Endlosschleifen:
- Reguläre Endpoints (`TrainingController`, `ImportController`) → forwarden zu ServerSync-Endpoints
- ServerSync-Endpoints speichern nur, forwarden nie

---

## Dateien

### Neu

| Datei | Zweck |
|-------|-------|
| `TrainingsplanServer.Interface/ServerSyncRequest.cs` | Shared DTO für den Request |
| `CyccessWeb/Services/ServerSyncOptions.cs` | Config-Klasse: ApiKey, TrainingsplanServerUrl |
| `CyccessWeb/Services/ServerApiKeyAttribute.cs` | IAuthorizationFilter für X-Server-ApiKey |
| `CyccessWeb/Services/TrainingsplanServerSyncService.cs` | Outbound: Session → JsonTrainingseinheit → POST |
| `CyccessWeb/Controllers/ServerSyncController.cs` | Inbound: JsonTrainingseinheit → Session speichern |
| `TrainingsplanServer/Services/ServerSyncOptions.cs` | Config-Klasse: ApiKey, CyccessWebUrl |
| `TrainingsplanServer/Services/ServerApiKeyAttribute.cs` | IAuthorizationFilter für X-Server-ApiKey |
| `TrainingsplanServer/Services/CyccessWebSyncService.cs` | Outbound: JsonTrainingseinheit → POST |
| `TrainingsplanServer/Controllers/ServerSyncController.cs` | Inbound: JsonTrainingseinheit → DatabaseTE speichern |

### Geändert

| Datei | Änderung |
|-------|----------|
| `CyccessWeb/CyccessWeb.csproj` | ProjectReference auf TrainingsplanServer.Interface |
| `CyccessWeb/Program.cs` | ServerSyncOptions binden, HttpClient + SyncService registrieren |
| `CyccessWeb/Controllers/TrainingController.cs` | `TrainingsplanServerSyncService` injizieren, nach Appointment/Session-Änderungen aufrufen |
| `TrainingsplanServer/Startup.cs` | ServerSyncOptions binden, HttpClient + SyncService registrieren |
| `TrainingsplanServer/Controllers/ImportController.cs` | `CyccessWebSyncService` injizieren, nach Cardio/Copy aufrufen; async machen |

---

## Verifizierung

1. **Build:** Beide Projekte fehlerfrei bauen
2. **Cardio → CyccessWeb:** POST /api/import/cardio auf TrainingsplanServer → CyccessWeb hat neue EnduranceExercise-Session
3. **Copy → CyccessWeb:** POST /api/import/copy auf TrainingsplanServer → CyccessWeb hat kopierte Session
4. **Appointment → TrainingsplanServer:** POST /api/training/appointments auf CyccessWeb → TrainingsplanServer hat neue TE
5. **Session-Update → TrainingsplanServer:** PUT /api/training/sessions auf CyccessWeb → TrainingsplanServer hat aktualisierte TE
6. **Unbekannter Athlet:** Daten werden stillschweigend verworfen (200 OK)
7. **Kein Loop:** ServerSync-Endpoints lösen keine weitere Weiterleitung aus
8. **Kein ApiKey konfiguriert:** ServerSync-Endpoints antworten 503