monitoring module has one read-only endpoint. It answers two questions in one call: how many trainees sit in each health bucket (training, nutrition, forms, app activity, payment), and which trainees match the bucket the coach clicked. It writes nothing.
Mount point and auth
Source:
apps/core-api/src/modules/monitoring/. The router has no role guard. It does not apply the coach access model from middleware/coach-access.ts: narrowing to one coach’s trainees happens only when the caller sends coachId.
GET /v1/web/monitoring
Auth: web lane, any role.
string
default:"training"
training, nutrition, forms, app or payment. Decides how bucket is read. It has no effect unless bucket is also sent.string
The bucket to list within
category. Valid names are in the table below. Without it the table lists all trainees that match coachId and search.string
Only trainees assigned to this coach through
ClientCoach (coachAssignments). Applies to the summary and to the table.string
Case-insensitive match on
name or email, or a substring match on phone. Applies to the table only. The summary ignores it.string
default:"week"
week (7 days), month (30 days) or all (365 days). The look-back window for workout and meal logs.integer
default:"1"
Positive integer.
integer
default:"25"
Positive integer, maximum 100.
Buckets
Thresholds are constants inmonitoring.service.ts. “In range” means since now - range.
PDF signature forms are excluded from the forms bucket because they wait on a signing link, not on the trainee app (see Document signing).
For
training buckets ok and under, and for nutrition bucket ok, the filter is built from the client ids in the summary groupBy result. When no trainee qualifies the filter becomes id in ['__none__'], which returns an empty page.
Response fields
integer
Trainees in scope for the summary: the studio’s clients, narrowed by
coachId if sent. Every client status is counted, including archived ones, because the base filter is only studioId and the coach.object
Bucket sizes per category, computed over the same scope as
total.object
One number per category for the tab badges:
training is none + under, nutrition is none, forms is pending, app is inactive + low, payment is expired + soon.object[]
One page of trainees ordered by
createdAt descending.integer
Echo of the query.
integer
Echo of the query.
integer
Total rows matching the table filter (
coachId, search and bucket). Use this for pagination, not total.Example
VALIDATION (422) for an invalid category, range, page or pageSize, and UNAUTHORIZED (401). An unknown bucket value is not an error: it falls through to the default branch of its category as described in the table.
How the query runs
- One parallel wave of eight queries builds the summary: the total, a
workoutLog.groupByand amealLog.groupByonclientId, and five counts. - The table filter is
ANDof the base filter (studio, coach, search) and the bucket filter. client.findManyandclient.countfetch the page andmatched.- A second wave of three queries loads workout counts, meal counts and pending form client ids for the ids on the page only.
Things to know
- The row field
statuses.paymentand thepaymentcounts readClient.endsOndirectly. They do not use the coverage end date that the dashboard uses for renewals, so a trainee with a queued next plan can show assoonhere. - The payment row status treats
daysRemainingof 0 asexpired. BecausedaysRemainingrounds up, a plan ending later today still shows 1 day andsoon. range=allis 365 days, not unbounded.