mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 22:26:13 +00:00
feat(api): expose workouts list + detail endpoints
Swap the v1.4.27 cluster picker on `GET /api/workouts` for the
source-priority-aware `pickCanonicalWorkoutRows()` (v1.4.30) so the
per-user ladder governs the canonical pick, fix the latent
Prisma-field-name bug (`distanceMeters` / `energyKcal` → the actual
`totalDistanceM` / `totalEnergyKcal` columns), and align the wire
shape with the iOS handoff contract (`distanceM`,
`activeEnergyKcal`, `avgHr`, `maxHr`).
Add `GET /api/workouts/{id}` for the workout-detail surface — owns
the optional `WorkoutRoute` GeoJSON geometry, exposes `canonicalId`
so deep-link redirects can land on the cluster winner, and seals
the existence channel with a cross-user 404 guard.
Register both routes in the OpenAPI registry so the pre-commit
drift hook catches the new shape going forward.
This commit is contained in:
1 parent
da15885dd4
commit
8d514ed127
6 files changed
+919
-80
No files matched your search
@@ -724,6 +724,23 @@ components:
|
||||
- data
|
||||
- error
|
||||
type: object
|
||||
GetWorkoutResponse:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/components/schemas/WorkoutDetailResponse"
|
||||
error:
|
||||
type: "null"
|
||||
meta:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- data
|
||||
- error
|
||||
type: object
|
||||
InsightsComprehensiveResponse:
|
||||
additionalProperties: false
|
||||
description: AI-generated insights bundle. Strict-schema validated server-side; Coach-routed when the insight surface
|
||||
@@ -831,6 +848,23 @@ components:
|
||||
- data
|
||||
- error
|
||||
type: object
|
||||
ListWorkoutsResponse:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/components/schemas/WorkoutListResponse"
|
||||
error:
|
||||
type: "null"
|
||||
meta:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- data
|
||||
- error
|
||||
type: object
|
||||
LoginResponse:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
@@ -1503,6 +1537,221 @@ components:
|
||||
- skipped
|
||||
- entries
|
||||
type: object
|
||||
WorkoutDetailResponse:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
activeEnergyKcal:
|
||||
anyOf:
|
||||
- type: number
|
||||
- type: "null"
|
||||
avgHr:
|
||||
anyOf:
|
||||
- maximum: 9007199254740991
|
||||
minimum: -9007199254740991
|
||||
type: integer
|
||||
- type: "null"
|
||||
canonicalId:
|
||||
type: string
|
||||
distanceM:
|
||||
anyOf:
|
||||
- type: number
|
||||
- type: "null"
|
||||
durationSec:
|
||||
maximum: 9007199254740991
|
||||
minimum: 0
|
||||
type: integer
|
||||
elevationM:
|
||||
anyOf:
|
||||
- type: number
|
||||
- type: "null"
|
||||
endedAt:
|
||||
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)))$
|
||||
type: string
|
||||
externalId:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
id:
|
||||
type: string
|
||||
maxHr:
|
||||
anyOf:
|
||||
- maximum: 9007199254740991
|
||||
minimum: -9007199254740991
|
||||
type: integer
|
||||
- type: "null"
|
||||
metadata:
|
||||
anyOf:
|
||||
- {}
|
||||
- type: "null"
|
||||
minHr:
|
||||
anyOf:
|
||||
- maximum: 9007199254740991
|
||||
minimum: -9007199254740991
|
||||
type: integer
|
||||
- type: "null"
|
||||
pauseDurationSec:
|
||||
anyOf:
|
||||
- maximum: 9007199254740991
|
||||
minimum: -9007199254740991
|
||||
type: integer
|
||||
- type: "null"
|
||||
route:
|
||||
anyOf:
|
||||
- $ref: "#/components/schemas/WorkoutRouteGeometry"
|
||||
- type: "null"
|
||||
source:
|
||||
enum:
|
||||
- MANUAL
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
type: string
|
||||
sportType:
|
||||
type: string
|
||||
startedAt:
|
||||
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)))$
|
||||
type: string
|
||||
stepCount:
|
||||
anyOf:
|
||||
- maximum: 9007199254740991
|
||||
minimum: -9007199254740991
|
||||
type: integer
|
||||
- type: "null"
|
||||
required:
|
||||
- id
|
||||
- sportType
|
||||
- startedAt
|
||||
- endedAt
|
||||
- durationSec
|
||||
- distanceM
|
||||
- activeEnergyKcal
|
||||
- avgHr
|
||||
- maxHr
|
||||
- minHr
|
||||
- stepCount
|
||||
- elevationM
|
||||
- pauseDurationSec
|
||||
- source
|
||||
- externalId
|
||||
- metadata
|
||||
- route
|
||||
- canonicalId
|
||||
type: object
|
||||
WorkoutListEntry:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
activeEnergyKcal:
|
||||
anyOf:
|
||||
- type: number
|
||||
- type: "null"
|
||||
avgHr:
|
||||
anyOf:
|
||||
- maximum: 9007199254740991
|
||||
minimum: -9007199254740991
|
||||
type: integer
|
||||
- type: "null"
|
||||
distanceM:
|
||||
anyOf:
|
||||
- type: number
|
||||
- type: "null"
|
||||
durationSec:
|
||||
maximum: 9007199254740991
|
||||
minimum: 0
|
||||
type: integer
|
||||
endedAt:
|
||||
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)))$
|
||||
type: string
|
||||
externalId:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
id:
|
||||
type: string
|
||||
maxHr:
|
||||
anyOf:
|
||||
- maximum: 9007199254740991
|
||||
minimum: -9007199254740991
|
||||
type: integer
|
||||
- type: "null"
|
||||
source:
|
||||
enum:
|
||||
- MANUAL
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
type: string
|
||||
sportType:
|
||||
type: string
|
||||
startedAt:
|
||||
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)))$
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- sportType
|
||||
- startedAt
|
||||
- endedAt
|
||||
- durationSec
|
||||
- distanceM
|
||||
- activeEnergyKcal
|
||||
- avgHr
|
||||
- maxHr
|
||||
- source
|
||||
- externalId
|
||||
type: object
|
||||
WorkoutListResponse:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
meta:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
droppedDuplicates:
|
||||
maximum: 9007199254740991
|
||||
minimum: 0
|
||||
type: integer
|
||||
limit:
|
||||
exclusiveMinimum: 0
|
||||
maximum: 9007199254740991
|
||||
type: integer
|
||||
offset:
|
||||
maximum: 9007199254740991
|
||||
minimum: 0
|
||||
type: integer
|
||||
total:
|
||||
maximum: 9007199254740991
|
||||
minimum: 0
|
||||
type: integer
|
||||
required:
|
||||
- total
|
||||
- limit
|
||||
- offset
|
||||
- droppedDuplicates
|
||||
type: object
|
||||
workouts:
|
||||
items:
|
||||
$ref: "#/components/schemas/WorkoutListEntry"
|
||||
type: array
|
||||
required:
|
||||
- workouts
|
||||
- meta
|
||||
type: object
|
||||
WorkoutRouteGeometry:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
geometry: {}
|
||||
sampleTimestamps:
|
||||
anyOf:
|
||||
- items:
|
||||
type: string
|
||||
type: array
|
||||
- type: "null"
|
||||
required:
|
||||
- geometry
|
||||
- sampleTimestamps
|
||||
type: object
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
bearerFormat: hlk_<64hex>
|
||||
@@ -2387,6 +2636,54 @@ paths:
|
||||
summary: Delete measurements by external ID (iOS deletion-sync)
|
||||
tags:
|
||||
- Measurements
|
||||
/api/workouts:
|
||||
get:
|
||||
description: "Paginated workout list with cross-source canonical-row dedup. The picker reads the per-user
|
||||
source-priority ladder and buckets by `(startedAt 5 min slot, sportType)` — twin workouts (Apple Watch +
|
||||
Withings ScanWatch) collapse to a single canonical row per cluster. Field names mirror the iOS handoff contract:
|
||||
`distanceM`, `activeEnergyKcal`, `avgHr`, `maxHr`."
|
||||
parameters:
|
||||
- in: query
|
||||
name: limit
|
||||
schema:
|
||||
maximum: 200
|
||||
minimum: 1
|
||||
type: integer
|
||||
- in: query
|
||||
name: offset
|
||||
schema:
|
||||
maximum: 9007199254740991
|
||||
minimum: 0
|
||||
type: integer
|
||||
- in: query
|
||||
name: since
|
||||
schema:
|
||||
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)))$
|
||||
type: string
|
||||
- in: query
|
||||
name: until
|
||||
schema:
|
||||
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)))$
|
||||
type: string
|
||||
- in: query
|
||||
name: sportType
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ListWorkoutsResponse"
|
||||
description: Canonical workout page.
|
||||
"401": *a1
|
||||
"422": *a2
|
||||
"429": *a3
|
||||
summary: List workouts (v1.4.32)
|
||||
tags:
|
||||
- Measurements
|
||||
/api/workouts/batch:
|
||||
post:
|
||||
description: Server-side ingest endpoint for HKWorkout records (iOS) and Withings activity rows (server-to-server). Up
|
||||
@@ -2570,6 +2867,36 @@ paths:
|
||||
summary: Typed workout batch ingest (v1.4.25 W16b)
|
||||
tags:
|
||||
- Measurements
|
||||
/api/workouts/{id}:
|
||||
get:
|
||||
description: Single-workout envelope. Owns the optional `WorkoutRoute` GeoJSON geometry + `canonicalId` pointer that
|
||||
resolves to the cluster winner so deep-links into non-canonical twin rows can redirect cleanly. Cross-user rows
|
||||
surface as 404 (existence channel sealed).
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/GetWorkoutResponse"
|
||||
description: Workout detail.
|
||||
"401": *a1
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
description: Workout not found (or owned by another user).
|
||||
"422": *a2
|
||||
"429": *a3
|
||||
summary: Workout detail (v1.4.32)
|
||||
tags:
|
||||
- Measurements
|
||||
servers:
|
||||
- description: Production
|
||||
url: https://healthlog.bombeck.io
|
||||
|
||||
Reference in new issue
Block a user