# Gym — what you can do

<!-- GENERATED by ops/tools/skill-sync — do not edit by hand. Run pnpm skills:sync. -->

<!-- GENERATED by ops/tools/skill-sync — do not edit by hand. Run `pnpm skills:sync`. -->

# Gym — what you can do

`/admin/d/gym` · pillar `apps`

Run a self-hosted gym — 1,324 exercises, weekly plans, guided sessions, 4 progression systems, AI coach.

## Fetch this, never remember it

These change per install and per release. Call for them - a list written here would be a future lie.

| What | Call | Why |
|---|---|---|
| this gym's exercise library, including its own custom exercises | `GET /api/gym/exercises` | the 1,324 shipped rows are the same everywhere, but a gym's custom exercises are not — and the facets tell you which filter combinations would return anything at all |
| whether a session is already running | `GET /api/gym/overview` | `running` is what makes Today resume rather than restart; starting a second session without `force` silently returns the first |
| what to lift next for one exercise, and why | `GET /api/gym/next/:exerciseId` | the prescription is DERIVED from the log every time it is asked for — never stored — so it is always current and there is no counter to go stale |
| whether this install can show exercise animations at all | `GET /api/gym/settings` | `mediaConfigured` is false by default and that is not a fault: the media belongs to Gym visual and Leumas ships none of it |

## How to actually do things here

Written by hand, because the order and the traps are the part no file can derive.

### Give somebody a program to start on

1. `GET /api/gym/starters` for the three shipped programs and what each would cost this catalog.
2. `POST /api/gym/routines` with `starterId` to build it, or `POST /api/gym/ai/program` to have one written.
3. Check `unfilled` (starter) or `warnings` (AI) before telling anyone it is ready.

**Trap:** A non-empty `unfilled` means the catalog could not supply those slots — the routine EXISTS and is missing exercises. Say so. The AI lane refuses the whole document rather than dropping an exercise it invented, so a 422 there means nothing was saved, not that something partial was.

### Run a workout

1. `POST /api/gym/sessions` with `{ routineId, day }` — it returns the sets already prescribed and stamped.
2. `PATCH /api/gym/sessions/:id/sets/:setId` with `{ reps, weight, completed: true }` per set as it happens.
3. `POST /api/gym/sessions/:id/finish` to close it and get the records it set.

**Trap:** An unticked set is a MISS, not an absence — that is what stops a session that fell apart from advancing the load next time. Finishing DELETES the unticked rows, so tick as you go rather than at the end. Sending `completed: false` un-ticks and changes the judgement.

### Work out why a weight went down

1. `GET /api/gym/next/:exerciseId` and read `prescription.reason.text` — it names the rule and the count.
2. `GET /api/gym/stats/exercise/:exerciseId` for the curve and the records behind it.

**Trap:** A deload is the policy working, not a bug. Greyskull resets after ONE failure by design where linear gives three attempts. A field the policy has no opinion on comes back `undefined`, not 0 — reading it as 0 prescribes an empty bar.

### Bring history over from another tracker

1. `POST /api/gym/import` with the file text and NO `commit` — this previews and returns `unresolved`.
2. Map each unresolved name to an exerciseId and re-post with `mapping` and `commit: true`.

**Trap:** A commit with unresolved names is REFUSED (409) unless you pass `ignoreUnresolved`, which DROPS those rows. Inventing an exercise is the one irreversible mistake an importer can make, so it will not guess. Weights convert per row, not per file.

### Show exercise animations

1. Obtain a licence from Gym visual, or host a copy you are entitled to.
2. `PATCH /api/gym/settings` with `mediaBase` set to that absolute https origin.

**Trap:** Leumas ships NO exercise media — it belongs to Gym visual and is excluded from the dataset's MIT grant. There is deliberately no default base, a relative path is refused, and the attribution must render wherever the media does. An empty `mediaBase` is the correct state, not a broken install.

## Tabs

Every one is a real URL. A tab with its own section below carries a real job.

| Tab | Path | Group | What it is for |
|---|---|---|---|
| `desk` | `/admin/gym` | - | - |
| `today` | `/admin/gym/today` | - | - |
| `plan` | `/admin/gym/plan` | - | - |
| `classes` | `/admin/gym/classes` | - | - |
| `library` | `/admin/gym/library` | - | - |
| `history` | `/admin/gym/history` | - | - |
| `stats` | `/admin/gym/stats` | - | - |
| `members` | `/admin/gym/members` | - | - |
| `settings` | `/admin/gym/settings` | - | - |

## API

Mounted prefixes that serve this domain: `/api/gym`

The full route table is `leumas-capabilities/reference/api-routes.md`.



---
Source: .claude/skills/leumas-studio/reference/domains/gym.md
Canonical: https://docs.leumas.tech/p/skills/leumas-studio/domains/gym
