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 userIDPOST /history/day— batch, keyed by userIDPOST /history/session— batch, keyed by sessionIDGET /users/{id}already embedsskillandtrainingGoal— 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
- 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.
- Empty windows return a valid DTO. An in-scope user with no sessions in
range is still included, with a
statswhose inner fields are null/zeroed. - Order is not meaningful — key by id, don't align by index.
- 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.