@trainheroic-unofficial/cli 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +21 -2
- package/package.json +6 -5
- package/skill/trainheroic-unofficial/SKILL.md +248 -0
- package/skill/trainheroic-unofficial/references/api-reference.md +1361 -0
- package/skill/trainheroic-unofficial/references/data-warehouse.md +127 -0
- package/skill/trainheroic-unofficial/references/workout-creation.md +310 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Warehouse store (design reference)
|
|
2
|
+
|
|
3
|
+
> **Note.** This documents the warehouse _design_ — the exercise mirror plus the
|
|
4
|
+
> programming/messaging history zones, the prune-vs-accumulate rule, `source`
|
|
5
|
+
> provenance, and the per-calendar month-window walk. That warehouse now lives in the
|
|
6
|
+
> hosted MCP server (`@trainheroic-unofficial/cloudflare`, D1). The CLI and the local
|
|
7
|
+
> server cache only the exercise library, to JSON at `~/.trainheroic/library.json`.
|
|
8
|
+
> The SQLite / `library_cache.py` / `*_sync.py` names below are historical; the schema
|
|
9
|
+
> and sync rules still apply to the hosted zones.
|
|
10
|
+
|
|
11
|
+
`library_cache.py` manages a SQLite database at `~/.trainheroic/library.db`
|
|
12
|
+
(mode 0600, outside any repo). It is both a read-through cache for exercise
|
|
13
|
+
lookups and a durable local store any future tool can open and query directly.
|
|
14
|
+
Stdlib `sqlite3` only — no dependencies.
|
|
15
|
+
|
|
16
|
+
Import it as a module (`from library_cache import ExerciseCache`) or use the CLI:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
library_cache.py resolve "<name>" # name -> exercise (exit 3 + candidates if ambiguous)
|
|
20
|
+
library_cache.py search "<query>" # ranked FTS search
|
|
21
|
+
library_cache.py get <id> # one exercise as JSON
|
|
22
|
+
library_cache.py sync [--force] # refresh the reference mirror from the API
|
|
23
|
+
library_cache.py stats # row counts per zone + cursors
|
|
24
|
+
library_cache.py cursors # the sync_state watermark table
|
|
25
|
+
library_cache.py create '<json>' # create a custom exercise (API) + write-through
|
|
26
|
+
library_cache.py forget <id> # drop an exercise from the mirror (cache-only)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Programming history and messages are synced by separate scripts:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
programming_sync.py # pull every calendar into the programming zone
|
|
33
|
+
programming_sync.py <cal_id> # one calendar/program id
|
|
34
|
+
messaging_sync.py # pull every chat stream into the messaging zone
|
|
35
|
+
messaging_sync.py --full # re-pull every stream from the beginning
|
|
36
|
+
messaging_sync.py <stream_id> # one stream id
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Zones
|
|
40
|
+
|
|
41
|
+
The DB holds three zones that follow deliberately different rules. The split is
|
|
42
|
+
load-bearing: applying the reference zone's prune logic to records would silently
|
|
43
|
+
delete history on a short fetch window.
|
|
44
|
+
|
|
45
|
+
### Reference zone — implemented
|
|
46
|
+
|
|
47
|
+
`exercise`, `tag`, `exercise_tag`, `swap`, `exercise_fts` (FTS5 over titles).
|
|
48
|
+
|
|
49
|
+
- Sourced from `GET /v5/exerciseLibrary/all` plus the tag endpoints.
|
|
50
|
+
- **Read-through with a 7-day TTL.** `ensure_fresh()` backfills when the DB is
|
|
51
|
+
empty or stale; a `resolve` miss forces one refresh and retries (catches
|
|
52
|
+
exercises added through the web UI out of band).
|
|
53
|
+
- **Prune-to-match.** Each full sync bumps a generation counter; rows the API no
|
|
54
|
+
longer returns are deleted — but only when the response has at least
|
|
55
|
+
`PRUNE_FLOOR` (100) rows, so a thin/garbled response never wipes the mirror.
|
|
56
|
+
- **Write-through hooks** `record_upsert(ex)` / `record_delete(id)` keep the
|
|
57
|
+
mirror correct after a create/update/delete without a full re-sync.
|
|
58
|
+
- IDs follow the API's own integers (Back Squat = 1, Bench Press = 1162);
|
|
59
|
+
id-less entries are skipped (this is the ~2384 → 2371 gap).
|
|
60
|
+
|
|
61
|
+
### Programming zone — implemented
|
|
62
|
+
|
|
63
|
+
`program`, `program_session`, `block`, `prescribed_set`, populated by
|
|
64
|
+
`programming_sync.py`.
|
|
65
|
+
|
|
66
|
+
- **Calendars to pull** = the union of `/1.0/coach/programs` (standalone) and each
|
|
67
|
+
team's `group_program` from `/1.0/coach/teams`.
|
|
68
|
+
- **Sourced from `GET /1.0/coach/programs/edit/{cal}/{y}/{m}/1`**, walked month by
|
|
69
|
+
month across a window (`MONTHS_BACK`/`MONTHS_FWD`): that endpoint returns only
|
|
70
|
+
the queried month's sessions, not the whole calendar.
|
|
71
|
+
- **Accumulate-only** — never pruned, so it retains history even after a session
|
|
72
|
+
is deleted on TrainHeroic. Each sync upserts every session and rebuilds that
|
|
73
|
+
session's own blocks/sets (delete + re-insert) to absorb edits, then advances
|
|
74
|
+
`sync_state('programming', <cal_id>)`. Re-running is idempotent.
|
|
75
|
+
- `prescribed_set` holds one row per prescribed set (the API's `param_N_data_1..10`
|
|
76
|
+
slots expanded). `prescribed_set.exercise_id` is a **soft** join to
|
|
77
|
+
`exercise(id)` — not an enforced FK, since the library cache may lag custom
|
|
78
|
+
exercises and a sync must not fail on a cache miss.
|
|
79
|
+
|
|
80
|
+
### Messaging zone — implemented
|
|
81
|
+
|
|
82
|
+
`message_stream`, `message_comment`, populated by `messaging_sync.py`.
|
|
83
|
+
|
|
84
|
+
- **Conversations** from `GET /v5/messaging/streams` (buckets `teams`, `athletes`,
|
|
85
|
+
`programs`, `coaches`). Each entry's `id` is the **stream id** — distinct from
|
|
86
|
+
`teamId`/`userId` and what every other messaging call keys on; `message_stream`
|
|
87
|
+
records `kind`, `team_id`, `user_id`, `last_viewed`.
|
|
88
|
+
- **Messages** from `GET /v5/messaging/streams/{id}/comments?lastCommentId={cursor}`,
|
|
89
|
+
walked incrementally: the cursor is the highest comment id stored, written to
|
|
90
|
+
`sync_state('messaging', stream_id)`, so a normal run only pulls newer comments.
|
|
91
|
+
`--full` passes a blank cursor to re-pull a whole stream (the only way to refresh
|
|
92
|
+
reactions/replies on already-synced comments, since the incremental call returns
|
|
93
|
+
newer-than-cursor only).
|
|
94
|
+
- **Accumulate-only** — comments are upserted by id and never pruned, so a message
|
|
95
|
+
soft-deleted on TrainHeroic is retained here as history (same rule as programming).
|
|
96
|
+
- `message_comment.is_author` is `0` for received messages, `1` for ones this coach
|
|
97
|
+
sent. `parent_id` holds a reply's parent comment id (threaded `replies[]` are
|
|
98
|
+
flattened into rows); `reactions` keeps the API's array as JSON.
|
|
99
|
+
- `message_send.py send` write-throughs the created comment so the store stays
|
|
100
|
+
correct without a re-sync; the next `messaging_sync.py` then advances the cursor.
|
|
101
|
+
|
|
102
|
+
## `sync_state` — incremental watermarks
|
|
103
|
+
|
|
104
|
+
One row per `(resource, scope_id)` — `scope_id` is an athlete/program id, or 0
|
|
105
|
+
for global. Columns: `cursor` (ISO date / high-water mark), `synced_at`,
|
|
106
|
+
`generation` (set only for prune-to-match zones). The library sync writes a
|
|
107
|
+
`library` cursor. Any new incremental sync must record its watermark here, not in
|
|
108
|
+
`sync_meta` (which holds only the library TTL timestamp and generation counter).
|
|
109
|
+
|
|
110
|
+
## Conventions for extending the store
|
|
111
|
+
|
|
112
|
+
- **Source tagging.** Every records table has `source` (default `'api'`). Write
|
|
113
|
+
synthetic/test rows with `source='seed'` so production queries can filter them
|
|
114
|
+
and a re-seed can delete only its own rows.
|
|
115
|
+
- **Pick the right zone rule.** Reference-style catalogs may prune-to-match;
|
|
116
|
+
anything historical must accumulate.
|
|
117
|
+
|
|
118
|
+
## Known limitations
|
|
119
|
+
|
|
120
|
+
- **No API write path for performance history.** Logged sets, readiness, and
|
|
121
|
+
working-max data are produced by the athlete mobile app; they are not creatable
|
|
122
|
+
or (as far as is known) syncable through this coach API. That is why
|
|
123
|
+
there is no performance zone here.
|
|
124
|
+
- **Units are per-athlete.** Weights live in each athlete's own unit (metric vs.
|
|
125
|
+
imperial). Normalize before any cross-athlete analytics.
|
|
126
|
+
- **`swaps` is empty from the bulk endpoint** — it likely only appears on
|
|
127
|
+
per-exercise detail or create responses.
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# Workout Creation Playbook
|
|
2
|
+
|
|
3
|
+
Building a workout is a multi-step sequence. Each step depends on an ID returned
|
|
4
|
+
by the previous one, so run them in order and capture the IDs. The exercise-add
|
|
5
|
+
step (step 4) is the one that returns HTTP 500 when fields are missing, so follow
|
|
6
|
+
its field list exactly.
|
|
7
|
+
|
|
8
|
+
All calls use the coach base (`https://api.trainheroic.com`) with the
|
|
9
|
+
`session-token` header. With the client: `$TH request POST <path> '<json>'`.
|
|
10
|
+
|
|
11
|
+
> **Prefer the builder.** the workout builder runs this whole sequence
|
|
12
|
+
> from a JSON spec and already fills the 500-prone fields and handles RPE
|
|
13
|
+
> correctly. Reach for the manual steps below only when debugging or doing
|
|
14
|
+
> something the builder does not cover.
|
|
15
|
+
>
|
|
16
|
+
> **RPE rule (verified the hard way):** never prescribe RPE as a structured param.
|
|
17
|
+
> `param_2_type: 14` is silently overridden to weight on any lift whose library
|
|
18
|
+
> default param is weight, so the RPE values render as pounds. Put RPE in the
|
|
19
|
+
> exercise `instruction` ("RPE 8"), prescribe reps, and leave load blank.
|
|
20
|
+
|
|
21
|
+
## Step 1 — Get a program/calendar to write to
|
|
22
|
+
|
|
23
|
+
Every team has an auto-created calendar (a "program"). Create a team if you need a
|
|
24
|
+
fresh target:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
POST /1.0/coach/team/createWithTitleAndCode
|
|
28
|
+
{ "title": "My Team" }
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The response and its side-effect loads expose the calendar/program ID (e.g.
|
|
32
|
+
`4713246`). Existing programs come from `GET /1.0/coach/programs` (use
|
|
33
|
+
`group_program` as the calendar ID for `/3.0/coach/program/{id}`).
|
|
34
|
+
|
|
35
|
+
## Step 2 — Create an empty session on a date
|
|
36
|
+
|
|
37
|
+
Date-based (team calendar):
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
POST /2.0/coach/calendar/workout/createWorkoutForDay/{calendarId}/{year}/{month}/{day}/0
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Timeline-based (relative-day programs):
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
POST /2.0/coach/calendar/workout/createWorkoutForTimelineDay/{programId}/{day}/null
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Pass an empty JSON body (`{}`). The response is a session object. Capture two
|
|
50
|
+
IDs from it (verified live):
|
|
51
|
+
|
|
52
|
+
- `workout_id` — the workout, needed by step 3 for blocks.
|
|
53
|
+
- `id` — the programWorkout id, needed by step 5 to publish (and by
|
|
54
|
+
`removeProgramWorkout` as `pwId`).
|
|
55
|
+
|
|
56
|
+
## Step 3 — Add a block to the session
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
POST /2.0/coach/calendar/saveProgramWorkoutSets
|
|
60
|
+
[{
|
|
61
|
+
"workout_id": 140318787,
|
|
62
|
+
"order": 1,
|
|
63
|
+
"type": 4,
|
|
64
|
+
"instruction": "",
|
|
65
|
+
"is_redzone": null,
|
|
66
|
+
"redzone_type": 0,
|
|
67
|
+
"exercises": [],
|
|
68
|
+
"exerciseKeys": [],
|
|
69
|
+
"key": "k::9292",
|
|
70
|
+
"title": "Strength/Power"
|
|
71
|
+
}]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`type` values: `1` = Conditioning, `2` = Hypertrophy, `4` = Strength/Power. The
|
|
75
|
+
endpoint accepts an array, so multiple blocks can be created in one call. The
|
|
76
|
+
response is an array of the created blocks; each block's `id` is its
|
|
77
|
+
`workout_set_id`, which step 4 needs (the `key` you send comes back as `null`,
|
|
78
|
+
so match blocks by `order`/`title`).
|
|
79
|
+
|
|
80
|
+
## Step 4 — Add exercises to the block (the 500-prone step)
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
POST /2.0/coach/calendar/saveWorkoutSetExercises
|
|
84
|
+
[ exercise1, exercise2, ... ]
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Every exercise object must include the full field set below. Omitting any of the
|
|
88
|
+
"critical" fields returns HTTP 500.
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"exercise_id": 1,
|
|
93
|
+
"workout_set_id": 671135671,
|
|
94
|
+
"set_id": 671135671,
|
|
95
|
+
"title": "Back Squat",
|
|
96
|
+
"instruction": "",
|
|
97
|
+
"order": 1,
|
|
98
|
+
"param_1_type": 3,
|
|
99
|
+
"param_2_type": 1,
|
|
100
|
+
"param_1_data_1": "5",
|
|
101
|
+
"param_1_data_2": "",
|
|
102
|
+
"param_1_data_3": "",
|
|
103
|
+
"param_1_data_4": "",
|
|
104
|
+
"param_1_data_5": "",
|
|
105
|
+
"param_1_data_6": "",
|
|
106
|
+
"param_1_data_7": "",
|
|
107
|
+
"param_1_data_8": "",
|
|
108
|
+
"param_1_data_9": "",
|
|
109
|
+
"param_1_data_10": "",
|
|
110
|
+
"param_2_data_1": "225",
|
|
111
|
+
"param_2_data_2": "",
|
|
112
|
+
"param_2_data_3": "",
|
|
113
|
+
"param_2_data_4": "",
|
|
114
|
+
"param_2_data_5": "",
|
|
115
|
+
"param_2_data_6": "",
|
|
116
|
+
"param_2_data_7": "",
|
|
117
|
+
"param_2_data_8": "",
|
|
118
|
+
"param_2_data_9": "",
|
|
119
|
+
"param_2_data_10": "",
|
|
120
|
+
"workout_set_exercise_template_id": null,
|
|
121
|
+
"no_sets": 0,
|
|
122
|
+
"param_count": 3,
|
|
123
|
+
"set_num": 3,
|
|
124
|
+
"key": "k::5001",
|
|
125
|
+
"setKey": 671135671,
|
|
126
|
+
"video_url": "",
|
|
127
|
+
"thumbnail_url": "",
|
|
128
|
+
"tags": [],
|
|
129
|
+
"eType": "e",
|
|
130
|
+
"use_count": 0
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Fields that cause a 500 when missing:
|
|
135
|
+
|
|
136
|
+
- `set_num` — number of sets (mirror `param_count`)
|
|
137
|
+
- `key` — any unique string in the form `"k::<number>"`
|
|
138
|
+
- `setKey` — equal to `workout_set_id`
|
|
139
|
+
- All 10 `param_1_data_N` and all 10 `param_2_data_N` slots — unused ones are `""`
|
|
140
|
+
- `eType` — `"e"` for an exercise
|
|
141
|
+
- `tags` — array, may be `[]`
|
|
142
|
+
- `video_url`, `thumbnail_url` — strings, may be `""`
|
|
143
|
+
- `use_count` — integer, may be `0`
|
|
144
|
+
- `workout_set_exercise_template_id` — `null`
|
|
145
|
+
|
|
146
|
+
The API ignores the `title` you send and substitutes the real title for
|
|
147
|
+
`exercise_id`. It may also override `param_2_type` from the exercise's defaults.
|
|
148
|
+
|
|
149
|
+
## Step 5 — Publish
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
POST /2.0/coach/calendar/programWorkout/publish
|
|
153
|
+
[ 142002657 ] // array of programWorkout IDs
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## (Optional) Session note — Coach Instructions
|
|
157
|
+
|
|
158
|
+
The session-level note (the day-note shown at the top of a session — greeting +
|
|
159
|
+
writeup) is set with a PUT to the programWorkout, not on a block. Use the same
|
|
160
|
+
`workout_id` from step 2:
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
PUT /3.0/coach/workout/{workout_id}
|
|
164
|
+
{ ...the full programWorkout object..., "instruction": "Welcome to Week 12..." }
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Build the body from the session object you already have (the create response, or a
|
|
168
|
+
day's entry from `/1.0/coach/programs/edit`), set `instruction`, and replace
|
|
169
|
+
`sets`/`setKeys` with a flat **list** of block ids sorted by `order` — the edit-GET
|
|
170
|
+
returns `sets` as a dict keyed by block id, so convert it. This does **not** publish:
|
|
171
|
+
`published` is echoed back as sent, so set the note **before** step 5 if the session
|
|
172
|
+
should stay a draft. the workout builder does all of this when the spec has a
|
|
173
|
+
top-level `"instruction"`.
|
|
174
|
+
|
|
175
|
+
## Reading a session back (verified)
|
|
176
|
+
|
|
177
|
+
To confirm what was built on a team calendar date:
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
GET /1.0/coach/programs/edit/{calendarId}/{year}/{month}/{day}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
This returns `programWorkouts`, an array of every session on the calendar (it is
|
|
184
|
+
not scoped to the single date you pass). Match the one you built by the `id` you
|
|
185
|
+
captured at create time — do not assume `programWorkouts[0]`. On the matched
|
|
186
|
+
session, `published` is `1` once published, and blocks live under its `sets`
|
|
187
|
+
object keyed by `workout_set_id`; each block has an `exercises` array with the
|
|
188
|
+
rendered `title`, `instruction`, and `param_*_data_N` values.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Parameter types
|
|
193
|
+
|
|
194
|
+
| Value | Meaning | Display |
|
|
195
|
+
| ----- | ------------------- | -------------- |
|
|
196
|
+
| 0 | None | (no parameter) |
|
|
197
|
+
| 1 | Weight | `225 lb` |
|
|
198
|
+
| 2 | Weight (% of max) | `75%` |
|
|
199
|
+
| 3 | Reps | `5` |
|
|
200
|
+
| 4 | Time (seconds) | `1:00` |
|
|
201
|
+
| 5 | Distance (yards) | `50yd` |
|
|
202
|
+
| 6 | Distance (meters) | `50m` |
|
|
203
|
+
| 7 | Height | inches |
|
|
204
|
+
| 10 | Distance (miles) | miles |
|
|
205
|
+
| 11 | Distance (feet) | feet |
|
|
206
|
+
| 12 | Height (inches) | inches |
|
|
207
|
+
| 13 | Heart Rate | bpm |
|
|
208
|
+
| 14 | RPE | rating |
|
|
209
|
+
| 18 | Time (seconds, alt) | `0:30` |
|
|
210
|
+
|
|
211
|
+
Most common combos: `p1=3,p2=1` (reps @ weight, e.g. Back Squat), `p1=3,p2=0`
|
|
212
|
+
(reps only / bodyweight, e.g. Push-Up), `p1=3` with `p2` absent (reps only),
|
|
213
|
+
`p1=5` (distance in yards), `p1=4` (time).
|
|
214
|
+
|
|
215
|
+
`param_1_data_N` is the value for param 1 in set N; `param_2_data_N` likewise for
|
|
216
|
+
param 2. `param_count` is the number of sets (max 10).
|
|
217
|
+
|
|
218
|
+
### The unit is fixed per exercise (verified)
|
|
219
|
+
|
|
220
|
+
On save the API **discards the `param_1_type`/`param_2_type` you send and restores
|
|
221
|
+
the exercise's library defaults.** Your `param_*_data_N` values are kept, so a value
|
|
222
|
+
sent under the wrong assumed unit silently renders under the exercise's real unit.
|
|
223
|
+
|
|
224
|
+
- **`param_1_type` (primary): always forced to the exercise default.** You cannot
|
|
225
|
+
change the primary unit at prescribe time. Stock `Run` (id 82) is miles, so a
|
|
226
|
+
"200 m run" written on `Run` renders as _200 miles_. For meters use a meters-native
|
|
227
|
+
exercise (`Sprint` 127, `Rowing` 101, `Shuttle Sprint` 42523) or a custom exercise.
|
|
228
|
+
- **`param_2_type` (secondary): forced to the default too, except** you may add weight
|
|
229
|
+
(`1`) to an exercise that has no secondary param (default `0`/none) — that is how
|
|
230
|
+
weighted Pull-Ups/Dips work. `2` (% of max) and `14` (RPE) do **not** stick on a
|
|
231
|
+
weight-default lift; both coerce to weight and render as pounds.
|
|
232
|
+
- Check an exercise's real units first: `$TH exercise resolve "<name>"` prints a
|
|
233
|
+
`units` array, ordered by entry slot (`[param 1, param 2]`). the workout builder also
|
|
234
|
+
prints a `WARNING` when a sent param type will be overridden, and its read-back labels
|
|
235
|
+
the stored units.
|
|
236
|
+
|
|
237
|
+
## Prescription patterns
|
|
238
|
+
|
|
239
|
+
- **Superset**: send multiple exercises in one array, same `workout_set_id`/
|
|
240
|
+
`set_id`, different `order`. They render as A1, A2, ...
|
|
241
|
+
- **Drop set**: one exercise, decreasing weight and increasing reps across the
|
|
242
|
+
`_data_N` slots.
|
|
243
|
+
- **Pyramid**: vary both params up then down across the slots
|
|
244
|
+
(e.g. reps `5,3,1,3,5` at weight `185,225,275,225,185`).
|
|
245
|
+
- **Bodyweight**: `param_2_type: 0` and leave every `param_2_data_N` empty.
|
|
246
|
+
- **Weighted bodyweight** (weighted Pull-Up/Dip): add `param_2_type: 1` with loads.
|
|
247
|
+
Adding weight sticks _only_ on exercises whose default secondary is none (`0`).
|
|
248
|
+
- **Max / AMRAP reps (verified)**: the rep slots are free text — put the literal
|
|
249
|
+
string `"Max"` (or `"AMRAP"`, `"ME"`) in `param_1_data_N`; it round-trips verbatim
|
|
250
|
+
and you can mix it with numbers (e.g. last set `"Max"`). The builder accepts
|
|
251
|
+
`"reps": ["Max", "Max"]` or `"reps": [5, 5, "Max"]`.
|
|
252
|
+
- **RPE and % of max (verified caveat)**: `param_2_type: 14` (RPE) and `2` (% of max)
|
|
253
|
+
do **not** stick on exercises whose library default param is weight — the API
|
|
254
|
+
overrides them back to weight, so the numbers render as pounds. Reliable approach:
|
|
255
|
+
prescribe reps with `param_2_type: 0` (load left blank for athlete autoregulation)
|
|
256
|
+
and put the target in the exercise `instruction` (e.g. `"RPE 8"` or `"75% of max"`).
|
|
257
|
+
The `instruction` field round-trips intact. (This is one case of the fixed-unit
|
|
258
|
+
rule above.)
|
|
259
|
+
|
|
260
|
+
## Common exercise IDs
|
|
261
|
+
|
|
262
|
+
| ID | Title | p1 | p2 |
|
|
263
|
+
| ---- | ----------- | --- | --- |
|
|
264
|
+
| 1 | Back Squat | 3 | 1 |
|
|
265
|
+
| 3 | Front Squat | 3 | 1 |
|
|
266
|
+
| 7 | Pull-Up | 3 | 0 |
|
|
267
|
+
| 24 | Burpee | 3 | 0 |
|
|
268
|
+
| 36 | Air Squat | 3 | 0 |
|
|
269
|
+
| 67 | Plank | 4 | 0 |
|
|
270
|
+
| 100 | Push-Up | 3 | 0 |
|
|
271
|
+
| 424 | Deadlift | 3 | 1 |
|
|
272
|
+
| 1162 | Bench Press | 3 | 1 |
|
|
273
|
+
|
|
274
|
+
Full library: `GET /v5/exerciseLibrary/all`. Custom exercises:
|
|
275
|
+
`POST /2.0/coach/exercise/create` with your own `title`, `param_1_type`,
|
|
276
|
+
`param_2_type`.
|
|
277
|
+
|
|
278
|
+
## Leaderboards (Red Zone)
|
|
279
|
+
|
|
280
|
+
A block can be a **leaderboard** — TrainHeroic's "Red Zone" competition score. The
|
|
281
|
+
UI shows a trophy and "FOR <UNIT>". It is encoded on the block, not the exercise,
|
|
282
|
+
and is a separate unit system (it has Feet/Meters/Calories even when the exercise
|
|
283
|
+
param is locked to another unit):
|
|
284
|
+
|
|
285
|
+
- `redzone_type` = the score unit; setting it `> 0` flags `is_redzone = 1`.
|
|
286
|
+
- `smaller_is_better = 1` makes the lowest score win (use for Time/Seconds).
|
|
287
|
+
- `redzone_instruction` = optional scoring note.
|
|
288
|
+
|
|
289
|
+
`redzone_type` values: `0` For Completion, `1` Weight, `2` Reps, `3` Rounds,
|
|
290
|
+
`4` Time, `5` Yards, `6` Meters, `7` Feet, `8` Calories, `10` Miles, `12` Inches,
|
|
291
|
+
`15` Watts, `17` Velocity, `18` Seconds.
|
|
292
|
+
|
|
293
|
+
In the workout builder add `"leaderboard"` to a block: a unit string (`"rounds"`,
|
|
294
|
+
`"time"`, `"calories"`, ...) or `{"unit": "time", "lowest_wins": true,
|
|
295
|
+
"instruction": "..."}`. Time/Seconds default to lowest-wins.
|
|
296
|
+
|
|
297
|
+
**When to set one (ask if unclear).** For an **AMRAP**, score = `rounds` (or `reps`);
|
|
298
|
+
for a **"for time"** workout, score = `time` (lowest wins); for a max-distance/row,
|
|
299
|
+
the distance/calorie unit. If the coach's intent is ambiguous, ask rather than guess.
|
|
300
|
+
For an AMRAP also program **multiple sets** (one per expected round) — assume a fit
|
|
301
|
+
athlete's round count (e.g. ~5–6 rounds for a ~10–12 min triplet) and confirm if unsure.
|
|
302
|
+
|
|
303
|
+
## Editing and managing sessions
|
|
304
|
+
|
|
305
|
+
- Set the session note (Coach Instructions): `PUT /3.0/coach/workout/{workoutId}`
|
|
306
|
+
with the programWorkout object + `instruction` (does not change publish state).
|
|
307
|
+
- Unpublish: `POST /2.0/coach/calendar/programWorkout/unPublish/{programWorkoutId}`
|
|
308
|
+
- Delete: `POST /2.0/coach/calendar/removeProgramWorkout` `{ "programId", "pwId" }`
|
|
309
|
+
- Copy/repeat to a date: `POST /2.0/coach/calendar/copyProgramWorkout`
|
|
310
|
+
- Save as template: `POST /2.0/coach/calendar/programWorkout/saveWorkoutAsTemplate/{workoutId}`
|