The 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.
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 in monitoring.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

Errors: 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

  1. One parallel wave of eight queries builds the summary: the total, a workoutLog.groupBy and a mealLog.groupBy on clientId, and five counts.
  2. The table filter is AND of the base filter (studio, coach, search) and the bucket filter.
  3. client.findMany and client.count fetch the page and matched.
  4. 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.payment and the payment counts read Client.endsOn directly. They do not use the coverage end date that the dashboard uses for renewals, so a trainee with a queued next plan can show as soon here.
  • The payment row status treats daysRemaining of 0 as expired. Because daysRemaining rounds up, a plan ending later today still shows 1 day and soon.
  • range=all is 365 days, not unbounded.