mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
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:
1 parent
d247f6966a
commit
30be0c4e0d
8 files changed
+463
-13
No files matched your search
@@ -125,7 +125,7 @@ coachPrefsSchema.meta({
|
||||
createBatchWorkoutSchema.meta({
|
||||
id: "CreateBatchWorkoutRequest",
|
||||
description:
|
||||
"v1.4.25 W16b — typed workout batch ingest. Each entry is an HKWorkout-aligned record with an optional nested GeoJSON LineString route. Up to 100 workouts per call; nested route geometry capped at 20 000 points. Withings server-to-server callers pass source: WITHINGS and ship no route (Withings reports aggregates only).",
|
||||
"Typed workout batch ingest. Each entry is an HKWorkout-aligned record with an optional nested GeoJSON LineString route AND an optional route-independent per-workout heart-rate series (`samples`: `[{ t, hr?, speedMs?, power?, cadence? }]`, up to 30 000 points). The `samples` series is the strain-engine input for indoor workouts that have no GPS route. Up to 100 workouts per call; nested route geometry capped at 20 000 points. Withings server-to-server callers pass source: WITHINGS and ship no route (Withings reports aggregates only).",
|
||||
});
|
||||
|
||||
const coachMessageFeedbackBody = z
|
||||
@@ -501,6 +501,17 @@ const workoutRouteGeometry = z
|
||||
})
|
||||
.meta({ id: "WorkoutRouteGeometry" });
|
||||
|
||||
// v1.10.0 — route-independent per-workout heart-rate series. Present
|
||||
// for indoor and outdoor workouts that shipped a `samples` array on
|
||||
// ingest. `samples` mirrors the ingest shape `[{ t, hr?, speedMs?,
|
||||
// power?, cadence? }]`; `sampleCount` is the denormalised length.
|
||||
const workoutHrSeries = z
|
||||
.object({
|
||||
sampleCount: z.number().int().nonnegative(),
|
||||
samples: z.unknown(),
|
||||
})
|
||||
.meta({ id: "WorkoutHrSeries" });
|
||||
|
||||
const workoutDetailResponse = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
@@ -520,6 +531,7 @@ const workoutDetailResponse = z
|
||||
externalId: z.string().nullable(),
|
||||
metadata: z.unknown().nullable(),
|
||||
route: workoutRouteGeometry.nullable(),
|
||||
samples: workoutHrSeries.nullable(),
|
||||
canonicalId: z.string(),
|
||||
})
|
||||
.meta({ id: "WorkoutDetailResponse" });
|
||||
|
||||
Reference in new issue
Block a user