--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+ https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>service-parent</artifactId>
+ <version>1.0.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>base-auth</artifactId>
+ <n>Base — Auth (Benutzerverwaltung, JWT, 2FA)</n>
+
+ <dependencies>
+ <dependency>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>base-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-security</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-mail</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.jsonwebtoken</groupId>
+ <artifactId>jjwt-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.jsonwebtoken</groupId>
+ <artifactId>jjwt-impl</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.jsonwebtoken</groupId>
+ <artifactId>jjwt-jackson</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>dev.samstevens.totp</groupId>
+ <artifactId>totp-spring-boot-starter</artifactId>
+ </dependency>
+ </dependencies>
+</project>
--- /dev/null
+package com.triathlon_coaching.product.auth.service;
+
+/**
+ * Benutzerverwaltung — plattformübergreifend nutzbar für alle Apps.
+ *
+ * Flows:
+ * Registrierung → E-Mail-Bestätigung → aktiver Account
+ * Login → JWT (30min) + Session-Token (30 Tage, DB-persistiert)
+ * 2FA → TOTP-Setup → QR-Code → Verify → aktiviert
+ * Passwort → Token per Mail → Reset
+ * Entitlement → Produkt-Freischaltung (z.B. 'swim_fit')
+ */
+public interface AuthService {
+
+ /** Registrierung: sendet Bestätigungsmail. */
+ void register(String email, String password, String displayName);
+
+ /** E-Mail-Token einlösen → Account aktiv. */
+ void verifyEmail(String token);
+
+ /**
+ * Login.
+ * @param totpCode null wenn 2FA nicht aktiv
+ * @return Session-Token (opakes, DB-persistiertes Token)
+ */
+ String login(String email, String password, String totpCode);
+
+ /** Session invalidieren. */
+ void logout(String sessionToken);
+
+ /** Passwort-Reset-Mail anfordern. */
+ void requestPasswordReset(String email);
+
+ /** Passwort-Reset-Token einlösen. */
+ void resetPassword(String token, String newPassword);
+
+ /**
+ * TOTP-Setup starten.
+ * @return otpauth://-URI für QR-Code-Generierung
+ */
+ String setupTotp(String userId);
+
+ /** TOTP aktivieren (nach QR-Code-Scan und erstem Code). */
+ void enableTotp(String userId, String totpCode);
+
+ /** TOTP deaktivieren (Passwortbestätigung erforderlich). */
+ void disableTotp(String userId, String currentPassword);
+
+ /** Prüft ob User ein Produkt-Entitlement hat (z.B. 'swim_fit'). */
+ boolean hasEntitlement(String userId, String productId);
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+ https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>service-parent</artifactId>
+ <version>1.0.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>base-calc</artifactId>
+ <n>Base — Calc (CSS, Zonen, Jack Daniels, GPX)</n>
+
+ <dependencies>
+ <dependency>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>base-core</artifactId>
+ </dependency>
+ <!-- GPX-Parsing -->
+ <dependency>
+ <groupId>io.jenetics</groupId>
+ <artifactId>jpx</artifactId>
+ <version>3.2.0</version>
+ </dependency>
+ </dependencies>
+</project>
--- /dev/null
+package com.triathlon_coaching.product.calc.swim;
+
+import org.springframework.stereotype.Service;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+/**
+ * Berechnet CSS-basierte Schwimmtempi.
+ * Direkter Port von swim_lib.inc.php: getZone(), getTempo(), speed2time(), toMin2()
+ *
+ * CSS = Critical Swim Speed, angegeben als Sekunden pro 100m.
+ */
+@Service
+public class CssCalculator {
+
+ /**
+ * CSS-Näherung aus 400m-Test.
+ * Entspricht: $testspeed = $strecke / $testzeit (mit strecke=400)
+ */
+ public double cssAusTest400(double test400Sek) {
+ return runden2(test400Sek / 4.0);
+ }
+
+ /**
+ * Präzise CSS nach Wakayoshi (1992): aus 200m + 400m Test.
+ * CSS = (400 - 200) / (t400 - t200) ergibt m/s → umrechnen auf sek/100m
+ */
+ public double cssAusTest200Und400(double t200Sek, double t400Sek) {
+ double speedMps = (400.0 - 200.0) / (t400Sek - t200Sek);
+ return runden2(100.0 / speedMps); // Sekunden pro 100m
+ }
+
+ /**
+ * Zielzeit für eine Strecke bei gegebenem CSS-Prozentsatz.
+ * Entspricht: speed2time($testspeed, $level, $distance)
+ *
+ * @param cssSek CSS in Sek/100m
+ * @param cssProzent Zonenintensität, z.B. 85 für 85% CSS
+ * @param streckeM Streckenlänge in Metern
+ * @return Zielzeit in Sekunden
+ */
+ public double zielzeit(double cssSek, int cssProzent, int streckeM) {
+ double speedMps = (100.0 / cssSek) * (cssProzent / 100.0);
+ return runden2(streckeM / speedMps);
+ }
+
+ /**
+ * Zielzeit für Offset-Tempo (+5s, -5s etc. pro 100m).
+ * Entspricht: getTempo($delta, ...) mit: $testzeit = $testzeit*100/$strecke + $delta
+ *
+ * @param cssSek CSS in Sek/100m
+ * @param deltaSek Offset in Sekunden pro 100m (positiv = langsamer)
+ * @param streckeM Streckenlänge in Metern
+ * @return Zielzeit in Sekunden
+ */
+ public double zeitzielMitOffset(double cssSek, int deltaSek, int streckeM) {
+ double tempoPro100m = cssSek + deltaSek;
+ return runden2(tempoPro100m * streckeM / 100.0);
+ }
+
+ /**
+ * Formatiert Sekunden als "M:SS"-String.
+ * Entspricht: toMin2($sekunden) in swim_lib.inc.php
+ */
+ public String formatMinSek(double sek) {
+ int minuten = (int) Math.floor(sek / 60);
+ double sekRest = Math.round((sek - minuten * 60.0) * 100.0) / 100.0;
+ if (sekRest >= 60) {
+ minuten++;
+ sekRest = 0;
+ }
+ return String.format("%d:%s", minuten,
+ sekRest < 10
+ ? String.format("0%.0f", sekRest)
+ : String.format("%.0f", sekRest));
+ }
+
+ /**
+ * Extrahiert Sekundenanteil als String (für Pausenangabe im PDF).
+ * Entspricht: getSecondsFromTime($time) — gibt "SS''" zurück.
+ */
+ public String formatPause(String timeStr) {
+ if (timeStr == null || timeStr.isBlank()) return "";
+ if (timeStr.length() >= 8) {
+ return timeStr.substring(6, 8) + "''";
+ }
+ return timeStr;
+ }
+
+ private double runden2(double val) {
+ return BigDecimal.valueOf(val).setScale(2, RoundingMode.HALF_UP).doubleValue();
+ }
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+ https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>service-parent</artifactId>
+ <version>1.0.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>base-core</artifactId>
+ <n>Base — Core (DB-Konfiguration, Basisklassen)</n>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-data-jpa</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.postgresql</groupId>
+ <artifactId>postgresql</artifactId>
+ <scope>runtime</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.flywaydb</groupId>
+ <artifactId>flyway-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.flywaydb</groupId>
+ <artifactId>flyway-database-postgresql</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-validation</artifactId>
+ </dependency>
+ </dependencies>
+</project>
--- /dev/null
+-- V1__initial_schema.sql
+-- Inhalt: vollständiges Schema aus schema_swim_triathlon_coaching.sql
+--
+-- Dieses File muss vor dem ersten Start mit dem Inhalt von
+-- schema_swim_triathlon_coaching.sql befüllt werden.
+--
+-- Flyway führt alle V*__*.sql Files in der Reihenfolge ihrer Versionsnummer aus.
+-- Weitere Migrationen: V2__..., V3__... etc.
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+ https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>service-parent</artifactId>
+ <version>1.0.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>base-user</artifactId>
+ <n>Base — User (Profil, Entitlements, CSS-Historie)</n>
+
+ <dependencies>
+ <dependency>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>base-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>base-auth</artifactId>
+ </dependency>
+ </dependencies>
+</project>
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
- <modelVersion>4.0.0</modelVersion>
- <groupId>de.laktatnebel.product</groupId>
- <artifactId>service.triathlon-coaching.com</artifactId>
- <version>0.0.1-SNAPSHOT</version>
- <packaging>pom</packaging>
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+ https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
- <name>service.triathlon-coaching.com</name>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>service-parent</artifactId>
+ <version>1.0.0-SNAPSHOT</version>
+ <packaging>pom</packaging>
+ <name>Triathlon Coaching — Service Parent</name>
- <parent>
- <groupId>de.laktatnebel.maven</groupId>
- <artifactId>spring-boot-starter-parent</artifactId>
- <version>3.3.1</version>
- </parent>
+ <parent>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-parent</artifactId>
+ <version>3.3.1</version>
+ <relativePath/>
+ </parent>
+ <modules>
+ <!-- Shared Platform Modules -->
+ <module>base-core</module>
+ <module>base-auth</module>
+ <module>base-calc</module>
+ <module>base-user</module>
+ <!-- Swim App Modules -->
+ <module>swim-domain</module>
+ <module>swim-pdf</module>
+ <module>swim-fit</module>
+ <module>swim-web</module>
+ </modules>
+
+ <properties>
+ <java.version>21</java.version>
+ <jjwt.version>0.12.5</jjwt.version>
+ <pdfbox.version>3.0.2</pdfbox.version>
+ <totp.version>1.7.1</totp.version>
+ <mapstruct.version>1.5.5.Final</mapstruct.version>
+ </properties>
+
+ <dependencyManagement>
+ <dependencies>
+ <!-- Shared Platform -->
+ <dependency>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>base-core</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>base-auth</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>base-calc</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>base-user</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <!-- Swim App -->
+ <dependency>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>swim-domain</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>swim-pdf</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>swim-fit</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <!-- JWT -->
+ <dependency>
+ <groupId>io.jsonwebtoken</groupId>
+ <artifactId>jjwt-api</artifactId>
+ <version>${jjwt.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>io.jsonwebtoken</groupId>
+ <artifactId>jjwt-impl</artifactId>
+ <version>${jjwt.version}</version>
+ <scope>runtime</scope>
+ </dependency>
+ <dependency>
+ <groupId>io.jsonwebtoken</groupId>
+ <artifactId>jjwt-jackson</artifactId>
+ <version>${jjwt.version}</version>
+ <scope>runtime</scope>
+ </dependency>
+ <!-- PDF -->
+ <dependency>
+ <groupId>org.apache.pdfbox</groupId>
+ <artifactId>pdfbox</artifactId>
+ <version>${pdfbox.version}</version>
+ </dependency>
+ <!-- TOTP / 2FA -->
+ <dependency>
+ <groupId>dev.samstevens.totp</groupId>
+ <artifactId>totp-spring-boot-starter</artifactId>
+ <version>${totp.version}</version>
+ </dependency>
+ <!-- MapStruct -->
+ <dependency>
+ <groupId>org.mapstruct</groupId>
+ <artifactId>mapstruct</artifactId>
+ <version>${mapstruct.version}</version>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+
+ <!-- Gemeinsame Abhängigkeiten für ALLE Module -->
+ <dependencies>
+ <dependency>
+ <groupId>org.projectlombok</groupId>
+ <artifactId>lombok</artifactId>
+ <optional>true</optional>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-test</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <pluginManagement>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <annotationProcessorPaths>
+ <path>
+ <groupId>org.projectlombok</groupId>
+ <artifactId>lombok</artifactId>
+ </path>
+ <path>
+ <groupId>org.mapstruct</groupId>
+ <artifactId>mapstruct-processor</artifactId>
+ <version>${mapstruct.version}</version>
+ </path>
+ </annotationProcessorPaths>
+ </configuration>
+ </plugin>
+ </plugins>
+ </pluginManagement>
+ </build>
</project>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+ https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>service-parent</artifactId>
+ <version>1.0.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>swim-domain</artifactId>
+ <n>Swim — Domain (Entities, Repositories, Generator)</n>
+
+ <dependencies>
+ <dependency>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>base-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>base-calc</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>base-user</artifactId>
+ </dependency>
+ </dependencies>
+</project>
--- /dev/null
+package com.triathlon_coaching.product.swim.domain;
+
+import jakarta.persistence.*;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.hibernate.annotations.JdbcTypeCode;
+import org.hibernate.type.SqlTypes;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+@EqualsAndHashCode(onlyExplicitlyIncluded = true)
+@Entity
+@Table(name = "block", schema = "swim")
+public class Block {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @EqualsAndHashCode.Include
+ private Integer id;
+
+ @Column(nullable = false, unique = true, length = 128)
+ private String name;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "fk_blockfunktion", nullable = false)
+ private Blockfunktion blockfunktion;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "fk_trainingsziel")
+ private Trainingsziel trainingsziel;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "fk_technikschwerpunkt")
+ private Technikschwerpunkt technikschwerpunkt;
+
+ private String detail;
+
+ /**
+ * 'serie' | 'pyramide' | 'intervall' | 'technik_drill'
+ * Pyramiden und Intervallmuster: strecken_muster enthält die Abfolge.
+ */
+ private String strukturtyp;
+
+ /**
+ * JSON-Array der Streckenlängen für Pyramiden/Intervalle.
+ * Beispiel Pyramide: [100, 150, 200, 150, 100]
+ * Beispiel Intervall: [100, 200, 100, 200, 100]
+ */
+ @JdbcTypeCode(SqlTypes.JSON)
+ @Column(name = "strecken_muster", columnDefinition = "jsonb")
+ private List<Integer> streckenMuster;
+
+ @OneToMany(mappedBy = "block",
+ cascade = CascadeType.ALL,
+ orphanRemoval = true,
+ fetch = FetchType.LAZY)
+ @OrderBy("position ASC")
+ private List<BlockUebung> uebungen = new ArrayList<>();
+}
--- /dev/null
+package com.triathlon_coaching.product.swim.domain;
+
+import jakarta.persistence.*;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+@EqualsAndHashCode(onlyExplicitlyIncluded = true)
+@Entity
+@Table(name = "trainingsplan", schema = "swim")
+public class Trainingsplan {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @EqualsAndHashCode.Include
+ private Integer id;
+
+ @Column(nullable = false, unique = true, length = 255)
+ private String name;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "fk_trainingsziel")
+ private Trainingsziel trainingsziel;
+
+ @Column(name = "umfang_meter")
+ private Integer umfangMeter;
+
+ @Column(name = "ist_generiert", nullable = false)
+ private boolean istGeneriert = false;
+
+ private String detail;
+
+ @Column(name = "created_at", nullable = false, updatable = false)
+ private Instant createdAt = Instant.now();
+
+ @OneToMany(mappedBy = "trainingsplan",
+ cascade = CascadeType.ALL,
+ orphanRemoval = true,
+ fetch = FetchType.LAZY)
+ @OrderBy("position ASC")
+ private List<TrainingsplanBlock> bloecke = new ArrayList<>();
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+ https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>service-parent</artifactId>
+ <version>1.0.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>swim-fit</artifactId>
+ <n>Swim — FIT (Garmin FIT-File-Erzeugung)</n>
+
+ <dependencies>
+ <dependency>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>swim-domain</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>base-calc</artifactId>
+ </dependency>
+ <!--
+ Garmin FIT SDK: manuell ins lokale Maven-Repo installieren.
+ Download: https://developer.garmin.com/fit/download/
+
+ mvn install:install-file \
+ -Dfile=FitSDKRelease_21.141.00.jar \
+ -DgroupId=com.garmin \
+ -DartifactId=fit-sdk \
+ -Dversion=21.141.00 \
+ -Dpackaging=jar
+ -->
+ <dependency>
+ <groupId>com.garmin</groupId>
+ <artifactId>fit-sdk</artifactId>
+ <version>21.141.00</version>
+ </dependency>
+ </dependencies>
+</project>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+ https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>service-parent</artifactId>
+ <version>1.0.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>swim-pdf</artifactId>
+ <n>Swim — PDF (Trainingsplan-PDF-Erzeugung)</n>
+
+ <dependencies>
+ <dependency>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>swim-domain</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>base-calc</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.pdfbox</groupId>
+ <artifactId>pdfbox</artifactId>
+ </dependency>
+ </dependencies>
+</project>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+ https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>service-parent</artifactId>
+ <version>1.0.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>swim-web</artifactId>
+ <packaging>jar</packaging>
+ <n>Swim — Web (Spring Boot App, deployable)</n>
+
+ <dependencies>
+ <!-- Swim App -->
+ <dependency>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>swim-domain</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>swim-pdf</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>swim-fit</artifactId>
+ </dependency>
+ <!-- Shared Platform -->
+ <dependency>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>base-auth</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>base-user</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.triathlon-coaching.product</groupId>
+ <artifactId>base-calc</artifactId>
+ </dependency>
+ <!-- Web -->
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-web</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-thymeleaf</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.thymeleaf.extras</groupId>
+ <artifactId>thymeleaf-extras-springsecurity6</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-actuator</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.mapstruct</groupId>
+ <artifactId>mapstruct</artifactId>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-maven-plugin</artifactId>
+ <configuration>
+ <excludes>
+ <exclude>
+ <groupId>org.projectlombok</groupId>
+ <artifactId>lombok</artifactId>
+ </exclude>
+ </excludes>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
--- /dev/null
+package com.triathlon_coaching.product.swim.web;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.domain.EntityScan;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.scheduling.annotation.EnableAsync;
+
+@SpringBootApplication(scanBasePackages = "com.triathlon_coaching.product")
+@EntityScan(basePackages = "com.triathlon_coaching.product")
+@EnableJpaRepositories(basePackages = "com.triathlon_coaching.product")
+@EnableAsync
+public class SwimApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(SwimApplication.class, args);
+ }
+}
--- /dev/null
+package com.triathlon_coaching.product.swim.web.controller;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * REST API für swim.triathlon-coaching.com
+ *
+ * Öffentlich (kein Login):
+ * GET /api/swim/plaene → Liste (filterbar, sortierbar)
+ * GET /api/swim/plaene/{name}/pdf → PDF mit %-Angaben
+ * GET /api/swim/plaene/{name}/pdf?css=83.5 → PDF mit persönlichen Zeiten
+ * POST /api/swim/css/berechnen → CSS aus Testzeit
+ * GET /api/swim/zonen?css=83.5 → alle Zonen mit Zeiten
+ *
+ * Auth + Entitlement 'swim_fit' erforderlich:
+ * GET /api/swim/plaene/{name}/fit → FIT-Download (sichtbar, aber gesperrt ohne Login)
+ *
+ * Auth erforderlich:
+ * GET /api/swim/mein/css → eigene CSS-Historie
+ * POST /api/swim/mein/css → CSS speichern
+ */
+@RestController
+@RequestMapping("/api/swim")
+public class SwimController {
+
+ @GetMapping("/plaene")
+ public ResponseEntity<?> getPlaene(
+ @RequestParam(required = false) String ziel,
+ @RequestParam(required = false) Integer minUmfang,
+ @RequestParam(required = false) Integer maxUmfang,
+ @RequestParam(defaultValue = "umfang") String sort,
+ @RequestParam(defaultValue = "asc") String direction) {
+ // TODO: TrainingsplanService.findAll(filter, sort)
+ return ResponseEntity.ok().build();
+ }
+
+ @GetMapping("/plaene/{name}/pdf")
+ public ResponseEntity<byte[]> getPdf(
+ @PathVariable String name,
+ @RequestParam(required = false) Double css,
+ @RequestParam(required = false) Double test400) {
+ // TODO: TrainingsplanPdfService.erzeugePdf(name, css)
+ return ResponseEntity.ok()
+ .header(HttpHeaders.CONTENT_DISPOSITION,
+ "attachment; filename=\"" + name.replace(" ", "_") + ".pdf\"")
+ .contentType(MediaType.APPLICATION_PDF)
+ .body(new byte[0]);
+ }
+
+ /**
+ * FIT-Download: Endpunkt ist immer erreichbar (für "Lockmittel"-Anzeige),
+ * aber ohne Entitlement kommt HTTP 402 Payment Required zurück.
+ */
+ @GetMapping("/plaene/{name}/fit")
+ public ResponseEntity<byte[]> getFit(
+ @PathVariable String name,
+ @RequestParam(required = false) Double css) {
+ // TODO: Security-Check @PreAuthorize("@entitlementService.hasEntitlement('swim_fit')")
+ // Ohne Login/Entitlement → 402 mit Hinweis auf Registration
+ return ResponseEntity.status(402).build();
+ }
+
+ @PostMapping("/css/berechnen")
+ public ResponseEntity<?> berechneCss(@RequestBody CssRequest request) {
+ // TODO: CssCalculator.cssAusTest400() oder cssAusTest200Und400()
+ return ResponseEntity.ok().build();
+ }
+
+ @GetMapping("/zonen")
+ public ResponseEntity<?> getZonen(@RequestParam double css) {
+ // TODO: ZonenRechner.berechneAlleZonen(zonenAusDb, css, standardStrecken)
+ return ResponseEntity.ok().build();
+ }
+
+ // ── Records für Request/Response ──────────────────────────────────
+
+ record CssRequest(
+ Double test400Sek, // 400m-Test in Sekunden
+ Double t200Sek, // 200m-Test (für präzise CSS-Berechnung)
+ Double t400Sek, // 400m-Test (für präzise CSS-Berechnung)
+ Double cssDirectSek // direkte CSS-Eingabe in Sek/100m
+ ) {}
+}
--- /dev/null
+spring:
+ datasource:
+ url: jdbc:postgresql://localhost:5432/triathlon_coaching
+ username: ${DB_USER}
+ password: ${DB_PASSWORD}
+ hikari:
+ maximum-pool-size: 10
+ connection-timeout: 30000
+ jpa:
+ hibernate:
+ ddl-auto: validate # Schema kommt von Flyway, nie von Hibernate
+ properties:
+ hibernate:
+ default_schema: swim
+ dialect: org.hibernate.dialect.PostgreSQLDialect
+ format_sql: true
+ show-sql: false
+ flyway:
+ schemas: swim,auth,calc
+ locations: classpath:db/migration
+ baseline-on-migrate: true
+ mail:
+ host: ${SMTP_HOST}
+ port: 587
+ username: ${SMTP_USER}
+ password: ${SMTP_PASSWORD}
+ properties:
+ mail.smtp.auth: true
+ mail.smtp.starttls.enable: true
+ thymeleaf:
+ cache: false # in Produktion auf true setzen
+
+server:
+ port: 8080
+ compression:
+ enabled: true
+ mime-types: text/html,text/css,application/javascript,application/json
+
+stc:
+ auth:
+ jwt-secret: ${JWT_SECRET}
+ jwt-expiry-minutes: 30
+ session-expiry-days: 30
+ base-url: https://swim.triathlon-coaching.com
+ mail-from: noreply@triathlon-coaching.com
+ pdf:
+ watermark: "(c) by triathlon-coaching.com"
+ logo-path: classpath:static/img/logo-02.png
+ fit:
+ enabled: true # false = FIT-Download deaktiviert (z.B. SDK nicht vorhanden)
+
+management:
+ endpoints:
+ web:
+ exposure:
+ include: health,info
+ endpoint:
+ health:
+ show-details: when-authorized
+
+logging:
+ level:
+ com.triathlon_coaching.product: INFO
+ org.springframework.security: WARN
+ org.flywaydb: INFO
+
+---
+# Profil: local (Entwicklung)
+spring:
+ config:
+ activate:
+ on-profile: local
+ datasource:
+ url: jdbc:postgresql://localhost:5432/triathlon_coaching_dev
+ jpa:
+ show-sql: true
+ thymeleaf:
+ cache: false
+logging:
+ level:
+ com.triathlon_coaching.product: DEBUG