Skip to main content

Backend handoff: per-athlete summary endpoints

Audience: backend developer. Status: ✅ delivered — see the shipped contract in athlete-summary-backend-response.md.

The portal is building a coach-facing athlete summary — an athlete's muscle usage (body-map tint, per-muscle bars, strength-quality zones) and their training goal / skill. The old iOS app has all of this, but every endpoint it uses is self-scoped (the logged-in athlete). A coach viewing athlete X needs the same data for a chosen athlete (or several). This document requests those variants.

The frontend already types and calls the self-scoped endpoints (see packages/core/src/api/history-stats.ts, docs/athlete-summary client work), so the ask is purely additive: same data, scoped by a userID list.

What exists today (self-scoped, unchanged)​

EndpointReturns
GET /history/v2/week?from=&to=HistoryStatsReturnDto for the signed-in user
GET /history/day?from=&to=HistoryStatsReturnDto for the signed-in user
GET /history/session/{id}HistoryStatsReturnDto for one session (already athlete-agnostic — a session belongs to a user)

from/to are Unix ms. HistoryStatsReturnDto = { created, volumeCompletion?, muscleMapVolume?, muscleUsage?, gridMetrics?, zoneDistribution? } (the shape the client already decodes).

What we need: batch, userID-scoped variants​

Same names, POST, request body carries a list of userIDs, response is a list of the same per-user DTO — one entry per requested user. No change to the per-user HistoryStatsReturnDto itself.

POST /history/v2/week​

// request
{ "userIDs": ["<uuid>", "<uuid>"], "from": 1730000000000, "to": 1730604800000 }
// response — keyed by userID preferred (see note)
[
{ "userID": "<uuid>", "stats": { /* HistoryStatsReturnDto */ } },
{ "userID": "<uuid>", "stats": { /* HistoryStatsReturnDto */ } }
]

POST /history/day​

Same request/response shape, day window.

POST /history/session (optional, lower priority)​

The per-session GET is already id-scoped, so a batch is only worth it if a coach view needs many sessions at once: { "sessionIDs": ["<uuid>", …] } → [{ "sessionID", "stats" }].

Response shape note: prefer the keyed-by-userID array above over a bare HistoryStatsReturnDto[] aligned by request index — it's robust to the backend reordering, deduping, or omitting a user with no data. If a requested user has no sessions in the window, return them with an empty/zeroed stats (or omit them — the client tolerates a missing entry).

Auth/scoping: the caller is a coach; the backend should enforce that each requested userID is an athlete the caller may view (same access rule as GET /users/{id}), and 403 / drop any they may not.

Also confirm (read-side): does the user read embed goal & skill?​

The coach summary shows the athlete's current training goal and skill. iOS gets these embedded on the user read (ENUser.CreateDto carries skill and trainingGoal objects). We've added optional skill? / trainingGoal? to UserReturnDto on the client — please confirm GET /users/{id} returns them embedded (or tell us the correct source). The selectable reference lists (GET /training_goals, GET /user_skills) and the goal set (PUT /users/{id}/training_goal/{goalId}) are already wired and unchanged.

Client swap plan (for context — no backend action)​

When the batch endpoints land, the frontend adds one function per range in the same module:

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

beside the existing self-scoped getOwnWeekHistoryStats / getOwnDayHistoryStats. The per-user DTO is unchanged, so the portal card simply reads the selected athlete's entry — no DTO or UI churn. This is why we ask for the same response DTO per user, just wrapped in a per-user list.