Skip to main content

Backend response: per-athlete summary endpoints

Audience: frontend. Status: shipped on develop. Replies to: athlete-summary-backend-handoff.md.

Batch, coach-scoped variants of the three history-summary endpoints are live, plus confirmation of the user-read question. No changes to the existing self-scoped GETs.

Summary​

  • POST /history/v2/week — batch, keyed by userID
  • POST /history/day — batch, keyed by userID
  • POST /history/session — batch, keyed by sessionID
  • GET /users/{id} already embeds skill and trainingGoal — no change needed

Per-user DTO is unchanged (HistoryStatsReturnDtoV2). (Paths above omit the /api prefix the base URL carries — that's how the client writes them.)

Endpoints​

POST /history/v2/week​

// request
{ "userIDs": ["<uuid>", "<uuid>"], "from": 1730000000000, "to": 1730604800000 }
// response — 200 OK
[
{ "userID": "<uuid>", "stats": { /* HistoryStatsReturnDtoV2 */ } },
{ "userID": "<uuid>", "stats": { /* HistoryStatsReturnDtoV2 */ } }
]

POST /history/day​

Same request/response shape; day window, matching the existing GET /history/day.

POST /history/session​

// request
{ "sessionIDs": ["<uuid>", "<uuid>"] }
// response — 200 OK
[ { "sessionID": "<uuid>", "stats": { /* HistoryStatsReturnDtoV2 */ } } ]

Access is enforced against the session's owning user; sessions outside the caller's scope are silently omitted.

Response semantics​

  1. Missing entries are expected. The array is not guaranteed to hold one entry per requested id — an id is omitted when the caller may not view the user (auth scope, silent — no 403) or the summary throws (logged server-side, dropped). Key the result by id and treat "no entry for X" as no data.
  2. Empty windows return a valid DTO. An in-scope user with no sessions in range is still included, with a stats whose inner fields are null/zeroed.
  3. Order is not meaningful — key by id, don't align by index.
  4. Duplicate ids in the request are collapsed silently.

Auth / scoping​

Same rule as GET /users/{id}: the requested user must be in the caller's sharing scope (UserService.getAccessToUserIds) — the caller owns the athlete or has them as a child user. Outside that is dropped without erroring the batch. Same userTokenProtected bearer middleware as the GETs; no new headers.

Performance notes​

  • Fan-out bounded to 8 concurrent per-user summaries per request. Large squads (20–40) won't saturate the DB pool but trade wall-clock for stability; ping backend to raise the limit or add a lighter endpoint if needed.
  • Session batch does one SELECT id, user_id … WHERE id IN (…) for the auth check before fanning out — no N+1 on scope.

Client wiring (delivered)​

packages/core/src/api/history-stats.ts folds each array into a Record keyed by id at the API boundary:

getUsersWeekHistoryStats(userIDs, from, to): Promise<Record<UUID, HistoryStatsReturnDtoV2>>
getUsersDayHistoryStats(userIDs, from, to): Promise<Record<UUID, HistoryStatsReturnDtoV2>>
getSessionsHistoryStats(sessionIDs): Promise<Record<UUID, HistoryStatsReturnDtoV2>>

The user-drawer Summary tab (AthleteSummaryPanel) requests a one-element batch for the drawer's athlete and renders "no data" when the id isn't a key. The self-scoped GETs remain for the debug self-view.