feat(workouts): accept an optional HR series on the batch contract

Extend POST /api/workouts/batch with an optional per-workout `samples`
array — a route-independent heart-rate series so indoor workouts still
yield a strain-relevant signal. Each entry is `{ t, hr?, speedMs?,
power?, cadence? }`; the array is capped at 30 000 points
(MAX_WORKOUT_HR_SAMPLES), covering a long endurance session sampled at
1 Hz while bounding the largest new workout-side write stream the same
way the 20 000-point route cap bounds the geometry.

Persistence rides the existing route-attachment path: the freshly
written workout id is resolved once (by composite key, or the manual
tuple), then both the route geometry and the HR series attach by FK
with skipDuplicates so a re-submitted batch stays idempotent. A workout
may carry both a GPS route and a canonical HR series, or just one.

GET /api/workouts/{id} returns the series alongside the route. OpenAPI
regenerated.

`samples` keeps first-write-wins/overwrite parity with the workout row
it hangs off: the 1:1 FK plus skipDuplicates means a re-ingest of an
existing workout never duplicates or mutates its series.
This commit is contained in:
Marc-André Bombeck committed 2026-06-02 22:02:46 +02:00
1 parent d247f6966a
commit 30be0c4e0d
8 files changed
+463 -13

No files matched your search

+46
View File
@@ -834,6 +834,35 @@ paths:
- t
required:
- geometry
samples:
minItems: 1
maxItems: 30000
type: array
items:
type: object
properties:
t:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
hr:
type: integer
minimum: 20
maximum: 300
speedMs:
type: number
minimum: 0
maximum: 50
power:
type: number
minimum: 0
maximum: 3000
cadence:
type: number
minimum: 0
maximum: 400
required:
- t
required:
- sportType
- startedAt
@@ -4322,6 +4351,10 @@ components:
anyOf:
- $ref: "#/components/schemas/WorkoutRouteGeometry"
- type: "null"
samples:
anyOf:
- $ref: "#/components/schemas/WorkoutHrSeries"
- type: "null"
canonicalId:
type: string
required:
@@ -4342,6 +4375,7 @@ components:
- externalId
- metadata
- route
- samples
- canonicalId
additionalProperties: false
WorkoutRouteGeometry:
@@ -4358,6 +4392,18 @@ components:
- geometry
- sampleTimestamps
additionalProperties: false
WorkoutHrSeries:
type: object
properties:
sampleCount:
type: integer
minimum: 0
maximum: 9007199254740991
samples: {}
required:
- sampleCount
- samples
additionalProperties: false
BatchWorkoutsResponse:
type: object
properties: