Schema export
This commit is contained in:
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Write|Edit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "FILE=$(jq -r '.tool_input.file_path'); echo \"$FILE\" | grep -q 'peakup-backend/src/main/java/com/peakup/.*\\.java$' || exit 0; cd /home/ingo/deve/peakUp && JAVA_HOME=/home/ingo/.jdks/temurin-21.0.7 mvn -f peakup-tools/pom.xml -s /home/ingo/.m2/settings_peak.xml -Dmaven.repo.local=/home/ingo/.m2/peak-repo compile -q && /home/ingo/.jdks/temurin-21.0.7/bin/java -cp peakup-tools/target/classes com.peakup.tools.generator.PackageInfoGenerator 2>/dev/null || true",
|
||||
"timeout": 60,
|
||||
"statusMessage": "Regenerating package-info files..."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
107
.claudeignore
107
.claudeignore
@@ -1,48 +1,77 @@
|
||||
# --- IDE & OS Specific ---
|
||||
**/.idea/
|
||||
**/.vscode/
|
||||
**/.settings/
|
||||
**/.classpath/
|
||||
**/.project/
|
||||
**/*.iml
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
**/testdata/**
|
||||
# =============================================
|
||||
# .claudeignore – Optimierte Version für peakUp (Pro Plan)
|
||||
# Ziel: Minimale automatische Datei-Reads + niedrige Burn-Rate
|
||||
# =============================================
|
||||
|
||||
# --- Frontend Tooling & Reports ---
|
||||
# === Build & Target Ordner (sehr wichtig!) ===
|
||||
**/target/
|
||||
**/dist/
|
||||
**/build/
|
||||
**.next/
|
||||
**.nuxt/
|
||||
|
||||
# === Node.js / Frontend Junk ===
|
||||
peakup-frontend/node_modules/
|
||||
peakup-frontend/dist/
|
||||
peakup-frontend/playwright-report/
|
||||
peakup-frontend/test-results/
|
||||
peakup-frontend/.cache/
|
||||
**/dist/**
|
||||
**/node_modules/**
|
||||
peakup-frontend/e2e/
|
||||
peakup-frontend/*.log
|
||||
peakup-frontend/package-lock.json
|
||||
peakup-frontend/pnpm-lock.yaml
|
||||
peakup-frontend/yarn.lock
|
||||
|
||||
# --- Temporary & Log Files ---
|
||||
*.log
|
||||
**/tmp/
|
||||
**/.temp/
|
||||
# === Maven & Java generierte Dateien ===
|
||||
**/mvnw
|
||||
**/mvnw.cmd
|
||||
**/*.iml # IntelliJ Projekt-Dateien
|
||||
**/.idea/
|
||||
**/*.class
|
||||
**/*.jar
|
||||
**/*.war
|
||||
**/*.ear
|
||||
|
||||
# Package Files #
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
*.sql
|
||||
*.json
|
||||
*.jsonl
|
||||
# === Logs, Cache, Temp ===
|
||||
**/*.log
|
||||
logs/
|
||||
log/
|
||||
tmp/
|
||||
temp/
|
||||
.cache/
|
||||
**/.cache/
|
||||
|
||||
# --- Infrastructure & Secrets (Safety) ---
|
||||
*.pem
|
||||
*.key
|
||||
.env*
|
||||
! .env.example
|
||||
# === Test & Coverage ===
|
||||
**/coverage/
|
||||
**/playwright-report/
|
||||
**/test-results/
|
||||
**/__tests__/
|
||||
**/e2e/
|
||||
|
||||
# --- Binary Assets ---
|
||||
# Logo bleibt im Repo, wird aber für Code-Analysen ignoriert
|
||||
# === Resources & große/nicht relevante Dateien ===
|
||||
resources/Logo_peakUp.png
|
||||
resources/*.json # strava-api.json & intervals.icu.json nur bei Bedarf lesen
|
||||
resources/*.sql # SQL-Skripte nur explizit mit @ laden
|
||||
|
||||
# === Lockfiles ===
|
||||
**/pom.xml.asc
|
||||
**/*.lock
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
bun.lockb
|
||||
|
||||
# === Dokumentation & Deploy (nicht ständig lesen) ===
|
||||
doc/
|
||||
deploy.sh
|
||||
deploy-frontend.sh
|
||||
README.md
|
||||
**/README.md
|
||||
|
||||
# === Sonstiges unnötiges ===
|
||||
**/.DS_Store
|
||||
**/*.tmp
|
||||
**/*.temp
|
||||
**/*.bak
|
||||
**/*.swp
|
||||
**/*.swo
|
||||
|
||||
# --- Wrapper & Scripts (Optional, falls Claude sie nicht editieren soll) ---
|
||||
# peakup-backend/mvnw
|
||||
# peakup-backend/mvnw.cmd
|
||||
15
.idea/checkstyle-idea.xml
generated
Normal file
15
.idea/checkstyle-idea.xml
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CheckStyle-IDEA" serialisationVersion="2">
|
||||
<checkstyleVersion>13.4.0</checkstyleVersion>
|
||||
<scanScope>JavaOnly</scanScope>
|
||||
<option name="thirdPartyClasspath" />
|
||||
<option name="activeLocationIds" />
|
||||
<option name="locations">
|
||||
<list>
|
||||
<ConfigurationLocation id="bundled-sun-checks" type="BUNDLED" scope="All" description="Sun Checks">(bundled)</ConfigurationLocation>
|
||||
<ConfigurationLocation id="bundled-google-checks" type="BUNDLED" scope="All" description="Google Checks">(bundled)</ConfigurationLocation>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
2
.idea/compiler.xml
generated
2
.idea/compiler.xml
generated
@@ -70,7 +70,7 @@
|
||||
</component>
|
||||
<component name="JavacSettings">
|
||||
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
|
||||
<module name="peakup-backend" options="-XDcompilePolicy=simple -XDshould-stop.ifError=FLOW "-Xplugin:ErrorProne -Xep:NullAway:ERROR -XepOpt:NullAway:AnnotatedPackages=com.peakup -XepOpt:NullAway:TreatGeneratedAsUnannotated=true -XepOpt:NullAway:CheckOptionalEmptiness=true -Xep:MissingSummary:OFF -Xep:MissingOverride:ERROR -Xep:DefaultCharset:ERROR -Xep:StringCaseLocaleUsage:ERROR -Xep:MathAbsoluteRandom:ERROR -Xep:UnnecessaryDefaultInEnumSwitch:WARN -Xep:OperatorPrecedence:WARN" -Xpkginfo:always -J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED -J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED -J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED -J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED -J--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED -J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED -J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED -J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED -J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED -J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED" />
|
||||
<module name="peakup-backend" options="-XDcompilePolicy=simple -XDshould-stop.ifError=FLOW "-Xplugin:ErrorProne -Xep:NullAway:ERROR -XepOpt:NullAway:AnnotatedPackages=com.peakup -Xep:MissingSummary:OFF -Xep:MissingOverride:ERROR -Xep:DefaultCharset:ERROR -Xep:StringCaseLocaleUsage:ERROR -Xep:MathAbsoluteRandom:ERROR -Xep:UnnecessaryDefaultInEnumSwitch:ERROR -Xep:OperatorPrecedence:ERROR -XepOpt:NullAway:TreatGeneratedAsUnannotated=true -XepOpt:NullAway:CheckOptionalEmptiness=true" -Xpkginfo:always -J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED -J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED -J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED -J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED -J--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED -J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED -J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED -J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED -J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED -J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED -Xmaxerrs 1000" />
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
1
.idea/inspectionProfiles/Project_Default.xml
generated
1
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -25,6 +25,7 @@
|
||||
<option name="m_ignoreImmediatelyReturnedVariables" value="false" />
|
||||
<option name="m_ignoreAnnotatedVariables" value="false" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="UnnecessaryUnboxing" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="UnusedProperty" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="VulnerableLibrariesLocal" enabled="false" level="WARNING" enabled_by_default="false">
|
||||
<option name="isIgnoringEnabled" value="true" />
|
||||
|
||||
116
CLAUDE.md
116
CLAUDE.md
@@ -1,90 +1,76 @@
|
||||
# CLAUDE.md — Project Guidelines
|
||||
# CLAUDE.md – peakUp Project Guidelines
|
||||
|
||||
**Last updated:** 2026-03-29
|
||||
**Status:** Single source of truth. 100% compliance required.
|
||||
**Last updated:** 2026-04-03 | Single source of truth. Strict compliance required.
|
||||
|
||||
## 1. Core Operating Rules
|
||||
## 1. Core Rules
|
||||
|
||||
- Use terminal tools (`ls`, `rg`, `mvn`, etc.) to validate state **before** and **after** changes.
|
||||
- Never assume; read file contents and package structures first.
|
||||
- **Clean Code:** Remove unreachable code immediately (ask user first).
|
||||
- **Trivial Tasks:** For renames/typos, ask: "Trivial? Direct edit or 4-Phase Workflow?"
|
||||
- Never assume. Always read files and structure first using tools.
|
||||
- Use terminal commands (`ls`, `rg`, `mvn`, `npm`) to validate before and after changes.
|
||||
- Remove unreachable/dead code immediately (ask user first if unsure).
|
||||
- For trivial tasks (renames, typos, small fixes): Ask "Trivial? Direct edit or full workflow?"
|
||||
|
||||
## 2. Mandatory Workflow (4-Phase)
|
||||
## 2. Mandatory 4-Phase Workflow
|
||||
|
||||
1. **PROPOSAL:** Clear description + before/after diff snippets + impact. Wait for `OK PROPOSAL`.
|
||||
2. **VALIDATE:** Brutal self-critique (NullAway, layering, regressions). Wait for `OK VALIDATE`.
|
||||
3. **IMPLEMENT:** One unit at a time. Run `PackageInfoGenerator` after new packages. Show exact diff.
|
||||
4. **VERIFICATION:** Propose/run tests (`$MVN test`). End with suggested commit message.
|
||||
1. **PROPOSAL** – Clear plan + before/after diff + impact. Wait for `OK PROPOSAL`.
|
||||
2. **VALIDATE** – Brutal self-critique (NullAway, layering, regressions). Wait for `OK VALIDATE`.
|
||||
3. **IMPLEMENT** – One unit at a time. Show exact diff.
|
||||
4. **VERIFICATION** – Run relevant tests. End with suggested commit message.
|
||||
|
||||
## 3. Project Structure & Conventions
|
||||
## 3. Project Structure
|
||||
|
||||
### Modules
|
||||
- **peakup-backend**: Quarkus (Java 21), Hibernate, PostgreSQL/TimescaleDB
|
||||
- **peakup-frontend**: Vite + React 18 + TypeScript + Tailwind + shadcn/ui
|
||||
- **peakup-tools**: Development generators
|
||||
- **peakup-model**: Shared domain models
|
||||
|
||||
- `peakup-backend`: Quarkus (Java 21), Hibernate, PostgreSQL/TimescaleDB.
|
||||
- `peakup-frontend`: Vite, React 18, TS, Tailwind, shadcn/ui.
|
||||
- `peakup-tools`: Dev-time generators.
|
||||
**Backend Architecture (com.peakup)**:
|
||||
|
||||
### Backend Architecture (com.peakup)
|
||||
- Domain slices (`activity`, `athlete`): `model` → `dao` → `service`
|
||||
- Connectors: `connector/<name>` with `model` → `dao` → `service` → `client` → `mapper`
|
||||
- All external mapping logic **must** live in `connector/<name>/mapper/`
|
||||
|
||||
- **Domain Slices** (`activity`, `athlete`): `model` (JPA) → `dao` (Criteria API) → `service`.
|
||||
- **Connectors** (`connector/<name>`): `model` → `dao` → `service` → `client` (Rest) → `mapper`.
|
||||
- **Mapping:** External DTO → Domain Entity **must** live in `connector/<name>/mapper/`.
|
||||
- **Streams:** TimescaleDB hypertables (`ActivityStreamPoint`) use `AbstractTimescaleEntity`.
|
||||
## 4. Important Technical Rules
|
||||
|
||||
### Base Classes & Patterns
|
||||
**Java / Backend:**
|
||||
|
||||
- `AbstractEntity`: Global sequence ID.
|
||||
- `AbstractTimestampedEntity`: Adds `createdAt`/`modifiedAt`.
|
||||
- **Strong Dependent:** `@Embeddable`, stored via `@ElementCollection` (e.g., `ActivityZone`).
|
||||
- **Dependent:** `@Entity` with own DAO, DB-level `ON DELETE CASCADE` (e.g., `ActivityStreamPoint`).
|
||||
- Java 21 + Quarkus
|
||||
- Strict NullAway compliance (`@Nullable`, `@SuppressWarnings("NullAway.Init")` only when necessary)
|
||||
- Lombok: `@Data`, `@NoArgsConstructor`, `@AllArgsConstructor`, `@Slf4j`
|
||||
- Checkstyle: 4 spaces, 120 char line limit, K&R braces, no star imports
|
||||
- Use `AbstractEntity` and `AbstractTimestampedEntity`
|
||||
- TimescaleDB hypertables via `AbstractTimescaleEntity`
|
||||
|
||||
### Integration Details
|
||||
**Frontend:**
|
||||
|
||||
- **Strava:** OAuth2 with token refresh in `StravaConnectorService`.
|
||||
- **Intervals.icu:** Basic Auth (API_KEY).
|
||||
- **Scheduler:** `ActivityImportScheduler` (every 20m) orchestrates all importers.
|
||||
- Light mode only (`#FFFFFF`)
|
||||
- Accent: red-to-orange gradient (`from-red-500 to-orange-500`) only for CTAs and charts
|
||||
- Use TanStack Query v5 + TanStack Table v8
|
||||
- Prefer Server Components / minimal `useEffect` for data fetching
|
||||
- Charts: Recharts (always responsive)
|
||||
|
||||
### Database & Code Quality
|
||||
## 5. Design Violations to Fix When Touching
|
||||
|
||||
- **DB:** Local `localhost:5432/peakup`. Flyway migrations in `db/migration/`.
|
||||
- **NullAway:** Enforced null-safety. Use `@Nullable` and `@SuppressWarnings("NullAway.Init")`.
|
||||
- **Lombok:** Use `@Data`, `@NoArgsConstructor`, `@AllArgsConstructor`, `@Slf4j`.
|
||||
- Move mapping logic from importers into dedicated mappers
|
||||
- Move `ActivityConnector` implementations into `connector/<name>/`
|
||||
- Extract `IntervalsEnrichmentStatusEnum` and `AbstractEnumConverter` properly
|
||||
|
||||
## 4. Frontend (Clean Geometric Performance)
|
||||
## 6. Useful Commands
|
||||
|
||||
- **Theme:** Light mode only (`#FFFFFF`). No `dark:` variants.
|
||||
- **Accent:** Red-to-orange gradient (`from-red-500 to-orange-500`) for CTAs/Charts only.
|
||||
- **Components:** `shadcn/ui` primitives. Max `rounded-sm`.
|
||||
- **State/Data:** TanStack Query v5, TanStack Table v8. No `useEffect` for fetching.
|
||||
- **Charts:** Recharts (always responsive). Linear gradients for curves.
|
||||
**Backend:**
|
||||
|
||||
## 5. Design Violations (Fix when touching)
|
||||
- `$MVN compile`
|
||||
- `$MVN quarkus:dev`
|
||||
- `$MVN test -Dtest=ClassName`
|
||||
|
||||
- **V1:** Move `ActivityConnector` implementations to `connector/<name>/`.
|
||||
- **V2/V3:** Extract mapping logic from importers into dedicated mappers.
|
||||
- **V4:** Move `IntervalsEnrichmentStatusEnum` to connector package to decouple domains.
|
||||
- **V5:** Move `AbstractEnumConverter` inner classes to their respective enums.
|
||||
**Frontend:**
|
||||
|
||||
## 6. Technical Rules & Checkstyle
|
||||
- `npm run dev`
|
||||
- `npm run build`
|
||||
|
||||
- **Java 21** strictly enforced.
|
||||
- **Checkstyle:** 4 spaces, 120 chars, K&R braces, no star imports.
|
||||
- **Import Order:** Non-java alpha, then `java.*`, then `jakarta.*`.
|
||||
- **Tests:** `@QuarkusTest` against real DB. Seed via `skripts.sql`.
|
||||
**Tools:**
|
||||
|
||||
## 7. Commands
|
||||
- PackageInfoGenerator (in `peakup-tools`)
|
||||
|
||||
```bash
|
||||
# Backend
|
||||
$MVN compile
|
||||
$MVN quarkus:dev
|
||||
$MVN test -Dtest=ClassName
|
||||
---
|
||||
|
||||
# Package Generator (from root)
|
||||
JAVA_HOME=... mvn -f peakup-tools/pom.xml ... compile
|
||||
... com.peakup.tools.generator.PackageInfoGenerator
|
||||
|
||||
# Frontend
|
||||
npm run dev
|
||||
npm run build
|
||||
**Style:** Be concise. Show only changed code + short explanation. Ask before large refactors.
|
||||
9
peakup-backend/.claudeignore
Normal file
9
peakup-backend/.claudeignore
Normal file
@@ -0,0 +1,9 @@
|
||||
# =============================================
|
||||
# .claudeignore – Optimierte Version für peakUp (Pro Plan)
|
||||
# Ziel: Minimale automatische Datei-Reads + niedrige Burn-Rate
|
||||
# =============================================
|
||||
target/
|
||||
*.class
|
||||
*.iml
|
||||
mvnw
|
||||
mvnw.cmd
|
||||
@@ -1,5 +1,5 @@
|
||||
<?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"
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
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>
|
||||
<groupId>com.peakup</groupId>
|
||||
@@ -118,7 +118,24 @@
|
||||
<artifactId>mapstruct</artifactId>
|
||||
<version>1.6.3</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.victools</groupId>
|
||||
<artifactId>jsonschema-generator</artifactId>
|
||||
<version>5.0.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.victools</groupId>
|
||||
<artifactId>jsonschema-module-jakarta-validation</artifactId>
|
||||
<version>5.0.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.victools</groupId>
|
||||
<artifactId>jsonschema-module-jackson</artifactId>
|
||||
<version>5.0.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-junit5</artifactId>
|
||||
@@ -166,11 +183,27 @@
|
||||
<configuration>
|
||||
<release>${maven.compiler.release}</release>
|
||||
<showWarnings>true</showWarnings>
|
||||
<fork>true</fork>
|
||||
<showDeprecation>true</showDeprecation>
|
||||
<!--fork>true</fork-->
|
||||
<forceLegacyJavacApi>true</forceLegacyJavacApi>
|
||||
<compilerArgs>
|
||||
<arg>-XDcompilePolicy=simple</arg>
|
||||
<arg>-XDshould-stop.ifError=FLOW</arg>
|
||||
<arg>-Xplugin:ErrorProne -Xep:NullAway:ERROR -XepOpt:NullAway:AnnotatedPackages=com.peakup -XepOpt:NullAway:TreatGeneratedAsUnannotated=true -XepOpt:NullAway:CheckOptionalEmptiness=true -Xep:MissingSummary:OFF -Xep:MissingOverride:ERROR -Xep:DefaultCharset:ERROR -Xep:StringCaseLocaleUsage:ERROR -Xep:MathAbsoluteRandom:ERROR -Xep:UnnecessaryDefaultInEnumSwitch:WARN -Xep:OperatorPrecedence:WARN</arg>
|
||||
<arg>
|
||||
-Xplugin:ErrorProne
|
||||
-Xep:NullAway:ERROR
|
||||
-XepOpt:NullAway:AnnotatedPackages=com.peakup
|
||||
-Xep:MissingSummary:OFF
|
||||
-Xep:MissingOverride:ERROR
|
||||
-Xep:DefaultCharset:ERROR
|
||||
-Xep:StringCaseLocaleUsage:ERROR
|
||||
-Xep:MathAbsoluteRandom:ERROR
|
||||
-Xep:UnnecessaryDefaultInEnumSwitch:ERROR
|
||||
-Xep:OperatorPrecedence:ERROR
|
||||
-XepOpt:NullAway:TreatGeneratedAsUnannotated=true
|
||||
-XepOpt:NullAway:CheckOptionalEmptiness=true
|
||||
</arg>
|
||||
|
||||
<arg>-Xpkginfo:always</arg>
|
||||
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
|
||||
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
|
||||
@@ -182,6 +215,8 @@
|
||||
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
|
||||
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
|
||||
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
|
||||
<arg>-Xmaxerrs</arg>
|
||||
<arg>1000</arg>
|
||||
</compilerArgs>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.peakup.common.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
@@ -21,6 +22,7 @@ public abstract class AbstractEntity {
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "global_seq")
|
||||
@SequenceGenerator(name = "global_seq", sequenceName = "global_id_seq", allocationSize = 1)
|
||||
@Column(name = "ID")
|
||||
@JsonIgnore
|
||||
private @Nullable Long id;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.peakup.common.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.MappedSuperclass;
|
||||
import jakarta.persistence.PrePersist;
|
||||
@@ -19,9 +20,11 @@ import java.time.Instant;
|
||||
public abstract class AbstractTimestampedEntity extends AbstractEntity {
|
||||
|
||||
@Column(nullable = false)
|
||||
@JsonIgnore
|
||||
private Instant modifiedAt = Instant.now();
|
||||
|
||||
@Column(nullable = false)
|
||||
@JsonIgnore
|
||||
private Instant createdAt = Instant.now();
|
||||
|
||||
@PrePersist
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.peakup.workout;
|
||||
|
||||
import com.peakup.workout.model.WeekPlan;
|
||||
import com.peakup.workout.model.WeekPlanImportRequest;
|
||||
import com.peakup.workout.model.Workout;
|
||||
import com.peakup.workout.service.WeekPlanImportService;
|
||||
import com.peakup.workout.service.WeekPlanService;
|
||||
@@ -127,7 +126,7 @@ public class WeekPlanResource {
|
||||
@Path("/import/schema")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response getImportSchema() {
|
||||
return serveClasspathResource("weekplan-import.schema.json", MediaType.APPLICATION_JSON);
|
||||
return Response.ok(weekPlanService.generateImportSchema()).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@@ -150,24 +149,36 @@ public class WeekPlanResource {
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{id}/export")
|
||||
public Response exportPlan(@PathParam("id") long id) {
|
||||
try {
|
||||
WeekPlan weekPlan = weekPlanService.getByIdWithWorkouts(id);
|
||||
return Response.ok(weekPlan).build();
|
||||
} catch (NoResultException e) {
|
||||
return Response.status(Response.Status.NOT_FOUND).build();
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/import")
|
||||
public Response importPlan(@QueryParam("athleteId") @Nullable Long athleteId,
|
||||
WeekPlanImportRequest req) {
|
||||
WeekPlan weekPlan) {
|
||||
if (athleteId == null) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity("athleteId query parameter is required").build();
|
||||
}
|
||||
List<String> errors = weekPlanImportService.validate(req);
|
||||
List<String> errors = weekPlanImportService.validate(weekPlan);
|
||||
if (!errors.isEmpty()) {
|
||||
return Response.status(Response.Status.BAD_REQUEST).entity(errors).build();
|
||||
}
|
||||
WeekPlan existing = weekPlanService.findByAthleteAndWeek(athleteId, req.getYear(), req.getCalendarWeek());
|
||||
WeekPlan existing = weekPlanService.findByAthleteAndWeek(
|
||||
athleteId, weekPlan.getYear(), weekPlan.getCalendarWeek());
|
||||
if (existing != null) {
|
||||
return Response.status(Response.Status.CONFLICT)
|
||||
.entity("A plan already exists for this week").build();
|
||||
}
|
||||
WeekPlan created = weekPlanImportService.mapAndCreate(athleteId, req);
|
||||
WeekPlan created = weekPlanImportService.create(athleteId, weekPlan);
|
||||
return Response.status(Response.Status.CREATED).entity(created).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ public enum IntensityReferenceEnum implements PersistableEnum<IntensityReference
|
||||
;
|
||||
|
||||
private final int key;
|
||||
@SuppressWarnings({"FieldCanBeLocal", "unused"})
|
||||
private final IntensityMetric intensityMetric;
|
||||
|
||||
IntensityReferenceEnum(int key, IntensityMetric intensityMetric) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.peakup.workout.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.peakup.common.model.AbstractTimestampedEntity;
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.persistence.Column;
|
||||
@@ -18,6 +19,7 @@ import java.util.List;
|
||||
uniqueConstraints = @UniqueConstraint(columnNames = {"athlete_id", "year", "calendar_week"}))
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class WeekPlan extends AbstractTimestampedEntity {
|
||||
|
||||
@Column(nullable = false)
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
package com.peakup.workout.model;
|
||||
|
||||
import jakarta.annotation.Nullable;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class WeekPlanImportRequest {
|
||||
|
||||
public int year;
|
||||
public int calendarWeek;
|
||||
public @Nullable String weekLabel;
|
||||
public @Nullable String weekDescription;
|
||||
public List<WorkoutImportItem> workouts = new ArrayList<>();
|
||||
|
||||
@Data
|
||||
public static class WorkoutImportItem {
|
||||
public @Nullable String title;
|
||||
public @Nullable String day;
|
||||
public @Nullable String time;
|
||||
public @Nullable String type;
|
||||
public @Nullable Integer estimatedTimeSeconds;
|
||||
public @Nullable Integer estimatedTss;
|
||||
public @Nullable String description;
|
||||
public @Nullable String statusNote;
|
||||
public List<StepImportItem> steps = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class StepImportItem {
|
||||
public @Nullable String label;
|
||||
public @Nullable String stepType;
|
||||
public int durationSeconds;
|
||||
public @Nullable String description;
|
||||
public @Nullable String intensityReference;
|
||||
public @Nullable Integer targetValuePct;
|
||||
public @Nullable Integer targetValuePctStart;
|
||||
public @Nullable Integer targetValuePctEnd;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.peakup.workout.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.peakup.common.model.AbstractEntity;
|
||||
import com.peakup.common.model.DependentEntity;
|
||||
import jakarta.annotation.Nullable;
|
||||
@@ -18,47 +19,38 @@ import java.util.List;
|
||||
@Table(name = "workout")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class Workout extends AbstractEntity implements DependentEntity {
|
||||
|
||||
@Column(name = "week_plan_id", nullable = false)
|
||||
public long weekPlanId;
|
||||
|
||||
@Override
|
||||
public long getFK() {
|
||||
return weekPlanId;
|
||||
}
|
||||
|
||||
@Column(nullable = false)
|
||||
public String title;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
public @Nullable String description;
|
||||
|
||||
@Column
|
||||
public @Nullable Instant workoutAt;
|
||||
|
||||
@Column(columnDefinition = "TEXT", nullable = false)
|
||||
public String description = "";
|
||||
@Column(nullable = false)
|
||||
public Instant workoutAt;
|
||||
@Column(nullable = false)
|
||||
public WorkoutTypeEnum workoutType;
|
||||
|
||||
@Column(nullable = false)
|
||||
public int sortOrder;
|
||||
|
||||
@Column(nullable = false)
|
||||
public WorkoutStatusEnum status = WorkoutStatusEnum.PLANNED;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
public @Nullable String statusNote;
|
||||
|
||||
@Column
|
||||
public @Nullable Integer estimatedTimeSeconds;
|
||||
|
||||
@Column(nullable = false)
|
||||
public int estimatedTimeSeconds;
|
||||
@Column
|
||||
public @Nullable Integer estimatedTss;
|
||||
|
||||
@Transient
|
||||
public List<WorkoutStep> steps = new ArrayList<>();
|
||||
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
public Workout() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFK() {
|
||||
return weekPlanId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.peakup.workout.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import com.peakup.common.model.AbstractEntity;
|
||||
import com.peakup.common.model.DependentEntity;
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Inheritance;
|
||||
@@ -27,35 +27,30 @@ import org.hibernate.annotations.DiscriminatorFormula;
|
||||
})
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public abstract class WorkoutStep extends AbstractEntity implements DependentEntity {
|
||||
|
||||
@Column(name = "workout_id", nullable = false)
|
||||
public long workoutId;
|
||||
@Column(nullable = false)
|
||||
public int sortIndex;
|
||||
@Column(nullable = false)
|
||||
public WorkoutStepTypeEnum stepType;
|
||||
@Column(nullable = false)
|
||||
public String label;
|
||||
@Column(columnDefinition = "TEXT", nullable = false)
|
||||
public String description = "";
|
||||
@Column(nullable = false)
|
||||
public int durationSeconds;
|
||||
@Column(nullable = false)
|
||||
public IntensityReferenceEnum intensityReference;
|
||||
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
protected WorkoutStep() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFK() {
|
||||
return workoutId;
|
||||
}
|
||||
|
||||
@Column(nullable = false)
|
||||
public int sortIndex;
|
||||
|
||||
@Column(nullable = false)
|
||||
public WorkoutStepTypeEnum stepType;
|
||||
|
||||
@Column(nullable = false)
|
||||
public String label;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
public @Nullable String description;
|
||||
|
||||
@Column(nullable = false)
|
||||
public int durationSeconds;
|
||||
|
||||
@Column
|
||||
public @Nullable IntensityReferenceEnum intensityReference;
|
||||
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
protected WorkoutStep() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
package com.peakup.workout.service;
|
||||
|
||||
import com.peakup.workout.model.FixedWorkoutStep;
|
||||
import com.peakup.workout.model.IntensityReferenceEnum;
|
||||
import com.peakup.workout.model.RampWorkoutStep;
|
||||
import com.peakup.workout.model.WeekPlan;
|
||||
import com.peakup.workout.model.WeekPlanImportRequest;
|
||||
import com.peakup.workout.model.Workout;
|
||||
import com.peakup.workout.model.WorkoutStatusEnum;
|
||||
import com.peakup.workout.model.WorkoutStep;
|
||||
import com.peakup.workout.model.WorkoutStepTypeEnum;
|
||||
import com.peakup.workout.model.WorkoutTypeEnum;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
|
||||
import java.time.DayOfWeek;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.temporal.IsoFields;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
@ApplicationScoped
|
||||
public class WeekPlanImportService {
|
||||
@@ -37,183 +28,87 @@ public class WeekPlanImportService {
|
||||
return year < currentYear || (year == currentYear && calendarWeek < currentWeek);
|
||||
}
|
||||
|
||||
public List<String> validate(WeekPlanImportRequest req) {
|
||||
public List<String> validate(WeekPlan plan) {
|
||||
List<String> errors = new ArrayList<>();
|
||||
if (req.getYear() < 2000 || req.getYear() > 2100) {
|
||||
if (plan.getYear() < 2000 || plan.getYear() > 2100) {
|
||||
errors.add("year: must be between 2000 and 2100");
|
||||
}
|
||||
if (req.getCalendarWeek() < 1 || req.getCalendarWeek() > 53) {
|
||||
if (plan.getCalendarWeek() < 1 || plan.getCalendarWeek() > 53) {
|
||||
errors.add("calendarWeek: must be between 1 and 53");
|
||||
}
|
||||
if (errors.isEmpty() && isPastWeek(req.getYear(), req.getCalendarWeek())) {
|
||||
if (errors.isEmpty() && isPastWeek(plan.getYear(), plan.getCalendarWeek())) {
|
||||
errors.add("Cannot import a plan for a past week");
|
||||
}
|
||||
if (req.getWorkouts().isEmpty()) {
|
||||
if (plan.getWorkouts().isEmpty()) {
|
||||
errors.add("workouts: must contain at least one workout");
|
||||
}
|
||||
for (int i = 0; i < req.getWorkouts().size(); i++) {
|
||||
validateWorkout(req.getWorkouts().get(i), i, errors);
|
||||
for (int i = 0; i < plan.getWorkouts().size(); i++) {
|
||||
validateWorkout(plan.getWorkouts().get(i), i, errors);
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
private void validateWorkout(WeekPlanImportRequest.WorkoutImportItem w, int idx, List<String> errors) {
|
||||
@SuppressWarnings("NullAway")
|
||||
private void validateWorkout(Workout w, int idx, List<String> errors) {
|
||||
String prefix = "workouts[" + idx + "]";
|
||||
//noinspection ConstantValue
|
||||
if (w.getTitle() == null || w.getTitle().isBlank()) {
|
||||
errors.add(prefix + ".title: required");
|
||||
}
|
||||
if (w.getDay() == null) {
|
||||
errors.add(prefix + ".day: required");
|
||||
} else {
|
||||
try {
|
||||
var ignored = DayOfWeek.valueOf(w.getDay().toUpperCase(Locale.ROOT));
|
||||
} catch (IllegalArgumentException e) {
|
||||
errors.add(prefix + ".day: must be one of monday, tuesday, wednesday,"
|
||||
+ " thursday, friday, saturday, sunday");
|
||||
}
|
||||
}
|
||||
if (w.getType() == null) {
|
||||
errors.add(prefix + ".type: required");
|
||||
} else {
|
||||
try {
|
||||
var ignored = WorkoutTypeEnum.valueOf(w.getType().toUpperCase(Locale.ROOT));
|
||||
} catch (IllegalArgumentException e) {
|
||||
errors.add(prefix + ".type: must be one of BIKE, RUN, SWIM, STRENGTH, OTHER");
|
||||
}
|
||||
}
|
||||
if (w.getTime() != null) {
|
||||
try {
|
||||
LocalTime.parse(w.getTime());
|
||||
} catch (Exception e) {
|
||||
errors.add(prefix + ".time: must be in HH:mm format");
|
||||
//noinspection ConstantValue
|
||||
if (w.getWorkoutAt() == null) {
|
||||
errors.add(prefix + ".workoutAt: required (ISO-8601 instant, e.g. 2026-04-01T06:30:00Z)");
|
||||
}
|
||||
//noinspection ConstantValue
|
||||
if (w.getWorkoutType() == null) {
|
||||
errors.add(prefix + ".workoutType: required (BIKE, RUN, SWIM, STRENGTH, OTHER)");
|
||||
}
|
||||
for (int j = 0; j < w.getSteps().size(); j++) {
|
||||
validateStep(w.getSteps().get(j), prefix + ".steps[" + j + "]", errors);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateStep(WeekPlanImportRequest.StepImportItem s, String prefix, List<String> errors) {
|
||||
if (s.getLabel() == null || s.getLabel().isBlank()) {
|
||||
@SuppressWarnings("NullAway")
|
||||
private void validateStep(WorkoutStep step, String prefix, List<String> errors) {
|
||||
//noinspection ConstantValue
|
||||
if (step.getLabel() == null || step.getLabel().isBlank()) {
|
||||
errors.add(prefix + ".label: required");
|
||||
}
|
||||
if (s.getDurationSeconds() <= 0) {
|
||||
if (step.getDurationSeconds() <= 0) {
|
||||
errors.add(prefix + ".durationSeconds: must be a positive integer");
|
||||
}
|
||||
if (s.getDescription() == null || s.getDescription().isBlank()) {
|
||||
//noinspection ConstantValue
|
||||
if (step.getDescription() == null || step.getDescription().isBlank()) {
|
||||
errors.add(prefix + ".description: required");
|
||||
}
|
||||
if (s.getIntensityReference() == null) {
|
||||
//noinspection ConstantValue
|
||||
if (step.getIntensityReference() == null) {
|
||||
errors.add(prefix + ".intensityReference: required");
|
||||
} else {
|
||||
try {
|
||||
var ignored = IntensityReferenceEnum.valueOf(s.getIntensityReference().toUpperCase(Locale.ROOT));
|
||||
} catch (IllegalArgumentException e) {
|
||||
errors.add(prefix + ".intensityReference: must be one of FTP, LTHR, THRESHOLD_PACE, MAX_HR");
|
||||
}
|
||||
}
|
||||
|
||||
WorkoutStepTypeEnum stepType = null;
|
||||
if (s.getStepType() == null) {
|
||||
//noinspection ConstantValue
|
||||
if (step.getStepType() == null) {
|
||||
errors.add(prefix + ".stepType: required");
|
||||
} else {
|
||||
try {
|
||||
stepType = WorkoutStepTypeEnum.valueOf(s.getStepType().toUpperCase(Locale.ROOT));
|
||||
} catch (IllegalArgumentException e) {
|
||||
errors.add(prefix + ".stepType: must be one of WARM_UP, WORK, RAMP, RECOVERY, REST, COOL_DOWN");
|
||||
}
|
||||
}
|
||||
|
||||
if (stepType == WorkoutStepTypeEnum.RAMP) {
|
||||
if (s.getTargetValuePctStart() == null) {
|
||||
} else if (step instanceof RampWorkoutStep ramp) {
|
||||
if (ramp.getTargetValuePctStart() == null) {
|
||||
errors.add(prefix + ".targetValuePctStart: required for RAMP steps");
|
||||
}
|
||||
if (s.getTargetValuePctEnd() == null) {
|
||||
if (ramp.getTargetValuePctEnd() == null) {
|
||||
errors.add(prefix + ".targetValuePctEnd: required for RAMP steps");
|
||||
}
|
||||
} else if (stepType != null) {
|
||||
if (s.getTargetValuePct() == null) {
|
||||
} else if (step instanceof FixedWorkoutStep fixed) {
|
||||
if (fixed.getTargetValuePct() == null) {
|
||||
errors.add(prefix + ".targetValuePct: required");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public WeekPlan mapAndCreate(long athleteId, WeekPlanImportRequest req) {
|
||||
WeekPlan plan = new WeekPlan();
|
||||
public WeekPlan create(long athleteId, WeekPlan plan) {
|
||||
plan.setAthleteId(athleteId);
|
||||
plan.setYear(req.getYear());
|
||||
plan.setCalendarWeek(req.getCalendarWeek());
|
||||
plan.setWeekLabel(req.getWeekLabel());
|
||||
plan.setWeekDescription(req.getWeekDescription());
|
||||
|
||||
LocalDate monday = weekMonday(req.getYear(), req.getCalendarWeek());
|
||||
List<Workout> workouts = new ArrayList<>();
|
||||
for (int i = 0; i < req.getWorkouts().size(); i++) {
|
||||
workouts.add(mapWorkout(req.getWorkouts().get(i), monday, i));
|
||||
List<Workout> workouts = plan.getWorkouts();
|
||||
for (int i = 0; i < workouts.size(); i++) {
|
||||
workouts.get(i).setSortOrder(i);
|
||||
}
|
||||
plan.setWorkouts(workouts);
|
||||
weekPlanService.createWeekPlan(plan);
|
||||
return plan;
|
||||
}
|
||||
|
||||
private Workout mapWorkout(WeekPlanImportRequest.WorkoutImportItem dto, LocalDate monday, int sortOrder) {
|
||||
Workout w = new Workout();
|
||||
w.setTitle(Objects.requireNonNull(dto.getTitle()));
|
||||
w.setDescription(dto.getDescription());
|
||||
w.setWorkoutType(WorkoutTypeEnum.valueOf(Objects.requireNonNull(dto.getType()).toUpperCase(Locale.ROOT)));
|
||||
w.setSortOrder(sortOrder);
|
||||
w.setStatus(WorkoutStatusEnum.PLANNED);
|
||||
w.setStatusNote(dto.getStatusNote());
|
||||
w.setEstimatedTimeSeconds(dto.getEstimatedTimeSeconds());
|
||||
w.setEstimatedTss(dto.getEstimatedTss());
|
||||
|
||||
String day = dto.getDay();
|
||||
if (day != null) {
|
||||
DayOfWeek dow = DayOfWeek.valueOf(day.toUpperCase(Locale.ROOT));
|
||||
LocalDate workoutDate = monday.plusDays(dow.getValue() - 1);
|
||||
LocalTime time = dto.getTime() != null ? LocalTime.parse(dto.getTime()) : LocalTime.of(0, 0);
|
||||
w.setWorkoutAt(workoutDate.atTime(time).toInstant(ZoneOffset.UTC));
|
||||
}
|
||||
|
||||
List<WorkoutStep> steps = new ArrayList<>();
|
||||
for (WeekPlanImportRequest.StepImportItem stepDto : dto.getSteps()) {
|
||||
steps.add(mapStep(stepDto));
|
||||
}
|
||||
w.setSteps(steps);
|
||||
return w;
|
||||
}
|
||||
|
||||
private WorkoutStep mapStep(WeekPlanImportRequest.StepImportItem dto) {
|
||||
WorkoutStepTypeEnum stepType = WorkoutStepTypeEnum.valueOf(
|
||||
Objects.requireNonNull(dto.getStepType()).toUpperCase(Locale.ROOT));
|
||||
|
||||
WorkoutStep step;
|
||||
if (stepType == WorkoutStepTypeEnum.RAMP) {
|
||||
RampWorkoutStep ramp = new RampWorkoutStep();
|
||||
ramp.setTargetValuePctStart(dto.getTargetValuePctStart());
|
||||
ramp.setTargetValuePctEnd(dto.getTargetValuePctEnd());
|
||||
step = ramp;
|
||||
} else {
|
||||
FixedWorkoutStep fixed = new FixedWorkoutStep();
|
||||
fixed.setTargetValuePct(dto.getTargetValuePct());
|
||||
step = fixed;
|
||||
}
|
||||
|
||||
step.setLabel(Objects.requireNonNull(dto.getLabel()));
|
||||
step.setDescription(dto.getDescription());
|
||||
step.setStepType(stepType);
|
||||
step.setDurationSeconds(dto.getDurationSeconds());
|
||||
|
||||
String intensityRef = dto.getIntensityReference();
|
||||
if (intensityRef != null) {
|
||||
step.setIntensityReference(IntensityReferenceEnum.valueOf(intensityRef.toUpperCase(Locale.ROOT)));
|
||||
}
|
||||
return step;
|
||||
}
|
||||
|
||||
private static LocalDate weekMonday(int year, int week) {
|
||||
return LocalDate.of(year, 1, 4)
|
||||
.with(IsoFields.WEEK_OF_WEEK_BASED_YEAR, week)
|
||||
.with(DayOfWeek.MONDAY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
package com.peakup.workout.service;
|
||||
|
||||
import com.github.victools.jsonschema.generator.OptionPreset;
|
||||
import com.github.victools.jsonschema.generator.SchemaGenerator;
|
||||
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
|
||||
import com.github.victools.jsonschema.generator.SchemaVersion;
|
||||
import com.github.victools.jsonschema.module.jackson.JacksonModule;
|
||||
import com.github.victools.jsonschema.module.jakarta.validation.JakartaValidationModule;
|
||||
import com.github.victools.jsonschema.module.jakarta.validation.JakartaValidationOption;
|
||||
import com.peakup.common.service.AbstractService;
|
||||
import com.peakup.workout.dao.WeekPlanDAO;
|
||||
import com.peakup.workout.dao.WorkoutDAO;
|
||||
@@ -111,4 +118,19 @@ public class WeekPlanService extends AbstractService<WeekPlan, WeekPlanDAO> {
|
||||
public void delete(WeekPlan entity) {
|
||||
super.delete(entity);
|
||||
}
|
||||
|
||||
public String generateImportSchema() {
|
||||
JakartaValidationModule validationModule = new JakartaValidationModule(
|
||||
JakartaValidationOption.NOT_NULLABLE_FIELD_IS_REQUIRED,
|
||||
JakartaValidationOption.INCLUDE_PATTERN_EXPRESSIONS
|
||||
);
|
||||
|
||||
@SuppressWarnings("removal") SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(
|
||||
SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON)
|
||||
.with(validationModule)
|
||||
.with(new JacksonModule());
|
||||
|
||||
SchemaGenerator generator = new SchemaGenerator(configBuilder.build());
|
||||
return generator.generateSchema(WeekPlan.class).toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "weekplan-import",
|
||||
"title": "WeekPlanImportRequest",
|
||||
"description": "AI-generated week plan import format for peakUp",
|
||||
"type": "object",
|
||||
"required": ["year", "calendarWeek", "workouts"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"year": {
|
||||
"type": "integer",
|
||||
"minimum": 2000,
|
||||
"maximum": 2100,
|
||||
"description": "ISO week-based year"
|
||||
},
|
||||
"calendarWeek": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 53,
|
||||
"description": "ISO calendar week number (1–53)"
|
||||
},
|
||||
"weekLabel": {
|
||||
"type": "string",
|
||||
"description": "Short label for the week, e.g. 'Race Prep Week 3'"
|
||||
},
|
||||
"weekDescription": {
|
||||
"type": "string",
|
||||
"description": "Longer description of the week's focus and goals"
|
||||
},
|
||||
"workouts": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"description": "Planned workouts. Omit rest days — do not add empty entries.",
|
||||
"items": { "$ref": "#/$defs/WorkoutImportItem" }
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"WorkoutImportItem": {
|
||||
"type": "object",
|
||||
"required": ["title", "day", "type"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Short name of the workout, e.g. 'Zone 2 Ride'"
|
||||
},
|
||||
"day": {
|
||||
"type": "string",
|
||||
"enum": ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"],
|
||||
"description": "Day of the week (lowercase)"
|
||||
},
|
||||
"time": {
|
||||
"type": "string",
|
||||
"pattern": "^\\d{2}:\\d{2}$",
|
||||
"description": "Start time in HH:mm format (24h), e.g. '07:30'. Optional — omit if no fixed start time."
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["BIKE", "RUN", "SWIM", "STRENGTH", "OTHER"],
|
||||
"description": "Sport type"
|
||||
},
|
||||
"estimatedTimeSeconds": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"description": "Total workout duration in seconds. Must equal the sum of all step durations when steps are provided."
|
||||
},
|
||||
"estimatedTss": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"description": "Estimated Training Stress Score. Formula: IF² × (estimatedTimeSeconds / 3600) × 100, where IF = avg intensity / threshold."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Optional longer description / coaching notes for the workout"
|
||||
},
|
||||
"statusNote": {
|
||||
"type": "string",
|
||||
"description": "Optional pre-populated status note"
|
||||
},
|
||||
"steps": {
|
||||
"type": "array",
|
||||
"description": "Ordered workout steps. If omitted the workout has no structured intervals.",
|
||||
"items": { "$ref": "#/$defs/StepImportItem" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"StepImportItem": {
|
||||
"type": "object",
|
||||
"required": ["label", "stepType", "durationSeconds", "description", "intensityReference"],
|
||||
"additionalProperties": false,
|
||||
"if": {
|
||||
"properties": { "stepType": { "const": "RAMP" } },
|
||||
"required": ["stepType"]
|
||||
},
|
||||
"then": {
|
||||
"required": ["targetValuePctStart", "targetValuePctEnd"]
|
||||
},
|
||||
"else": {
|
||||
"required": ["targetValuePct"]
|
||||
},
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "string",
|
||||
"description": "Short name, e.g. 'Warm Up', '4×8 min threshold', 'Recovery'"
|
||||
},
|
||||
"stepType": {
|
||||
"type": "string",
|
||||
"enum": ["WARM_UP", "WORK", "RAMP", "RECOVERY", "REST", "COOL_DOWN"],
|
||||
"description": "Step category. RAMP = progressive intensity change (requires targetValuePctStart + targetValuePctEnd)."
|
||||
},
|
||||
"durationSeconds": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"description": "Duration of this step in seconds"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Optional coaching cue for this step, e.g. 'Keep cadence above 90 rpm'"
|
||||
},
|
||||
"intensityReference": {
|
||||
"type": "string",
|
||||
"enum": ["FTP", "LTHR", "THRESHOLD_PACE", "MAX_HR"],
|
||||
"description": "Reference metric for target intensity. FTP = cycling power, LTHR = lactate-threshold HR, THRESHOLD_PACE = run threshold pace, MAX_HR = maximum HR."
|
||||
},
|
||||
"targetValuePct": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 200,
|
||||
"description": "Target intensity as % of intensityReference. Use for all stepTypes except RAMP."
|
||||
},
|
||||
"targetValuePctStart": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 200,
|
||||
"description": "Ramp start intensity (% of intensityReference). RAMP steps only."
|
||||
},
|
||||
"targetValuePctEnd": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 200,
|
||||
"description": "Ramp end intensity (% of intensityReference). RAMP steps only."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.peakup;
|
||||
|
||||
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
|
||||
import org.testcontainers.containers.PostgreSQLContainer;
|
||||
import org.testcontainers.postgresql.PostgreSQLContainer;
|
||||
import org.testcontainers.utility.DockerImageName;
|
||||
|
||||
import java.util.Map;
|
||||
@@ -12,8 +12,8 @@ public class TimescaleDBResource implements QuarkusTestResourceLifecycleManager
|
||||
.parse("timescale/timescaledb:latest-pg16")
|
||||
.asCompatibleSubstituteFor("postgres");
|
||||
|
||||
private static final PostgreSQLContainer<?> DB =
|
||||
new PostgreSQLContainer<>(IMAGE)
|
||||
private static final PostgreSQLContainer DB =
|
||||
new PostgreSQLContainer(IMAGE)
|
||||
.withDatabaseName("peakup")
|
||||
.withUsername("peakup")
|
||||
.withPassword("peakup");
|
||||
|
||||
51478
peakup-backend/src/test/resources/testdata/seed_data.jsonl
vendored
51478
peakup-backend/src/test/resources/testdata/seed_data.jsonl
vendored
File diff suppressed because it is too large
Load Diff
11
peakup-frontend/.claudeignore
Normal file
11
peakup-frontend/.claudeignore
Normal file
@@ -0,0 +1,11 @@
|
||||
# =============================================
|
||||
# .claudeignore – Optimierte Version für peakUp (Pro Plan)
|
||||
# Ziel: Minimale automatische Datei-Reads + niedrige Burn-Rate
|
||||
# =============================================
|
||||
node_modules/
|
||||
dist/
|
||||
playwright-report/
|
||||
test-results/
|
||||
e2e/
|
||||
*.log
|
||||
package-lock.json
|
||||
Reference in New Issue
Block a user