Skip to content

Commit 00b2557

Browse files
authored
Merge pull request #7 from codee-sh/release/v1.3.0
release: v1.3.0
2 parents 378342a + 7688b62 commit 00b2557

37 files changed

Lines changed: 14196 additions & 101 deletions
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
# Client note for the whole exercise (`exercise-logs`)
2+
3+
**Date:** 2026-06-20
4+
**Status:** implemented
5+
**Area:** backend + admin/frontend (Payload CMS / Next.js)
6+
7+
---
8+
9+
## TLDR
10+
11+
Today a client can only add a note **per set** (`set-logs.note`). We add a new log collection **`exercise-logs`** that stores **one client note for the whole exercise within a session** — symmetric to `set-logs`, but at the exercise level (`workout-exercise-rows`) rather than a single set.
12+
13+
---
14+
15+
## Open Questions
16+
17+
> All key decisions were resolved during the requirements discussion. No blocking open questions remain.
18+
19+
- ~~Layer: plan vs log?~~**log** (client note, not a coach template).
20+
- ~~Level: set / group / exercise?~~**exercise** (`workout-exercise-rows`).
21+
- ~~Data location: field on `workout-logs` vs new collection?~~**new collection `exercise-logs`** (Option B — normalized, consistent with `set-logs`).
22+
23+
---
24+
25+
## Problem Statement
26+
27+
In the workout tracker a client logs exercise execution as sets (`set-logs`). Each set can have its own note (`set-logs.note`, the field in [SeriesForm](../../src/components/workout/series-form/series-form.tsx)). What is missing is a note **for the whole exercise** — an annotation like "shoulders felt weak today, lower the weight next time" that applies to the exercise as a whole, not to one specific set.
28+
29+
The note cannot live in the plan: `workout-exercise-rows` has `update: isAdmin` — clients do not write to the template. It must therefore be created in the log layer, tied to the **(session, exercise)** pair.
30+
31+
Disambiguating the existing "notes" (the source of confusion):
32+
33+
| Note | Entity | Author | Scope |
34+
|---|---|---|---|
35+
| Plan note — exercise | `workout-exercise-rows.note` | coach | exercise template |
36+
| Log note — set | `set-logs.note` | client | one set |
37+
| **Log note — exercise** | **`exercise-logs.note` (NEW)** | **client** | **whole exercise in a session** |
38+
39+
---
40+
41+
## Proposed Solution
42+
43+
A new `exercise-logs` collection in the log layer, with the **same relations as `set-logs`** (`session`, `client`, `exercise`, `exerciseName`, `exerciseRow`, `roundLog`) plus a `note` — so it can grow beyond a note later without another schema change. It is keyed by `(session, exerciseRow)`: one note per exercise per session, enforced via an **upsert** pattern in the UI. The tracker loads the notes together with sets, and `ExerciseCard` displays and edits the note in the exercise header (not in the set form).
44+
45+
### Diagram — data model (plan layer vs log layer)
46+
47+
```mermaid
48+
erDiagram
49+
WORKOUTS ||--o{ WORKOUT_GROUPS : "has"
50+
WORKOUT_GROUPS ||--o{ WORKOUT_EXERCISE_ROWS : "has"
51+
52+
WORKOUT_LOGS ||--o{ SET_LOGS : "has"
53+
WORKOUT_LOGS ||--o{ EXERCISE_LOGS : "has (NEW)"
54+
55+
WORKOUT_EXERCISE_ROWS ||--o{ SET_LOGS : "logged as"
56+
WORKOUT_EXERCISE_ROWS ||--o{ EXERCISE_LOGS : "per-exercise note (NEW)"
57+
58+
WORKOUT_EXERCISE_ROWS {
59+
text note "PLAN note (coach)"
60+
}
61+
SET_LOGS {
62+
relationship session
63+
relationship exerciseRow
64+
text note "LOG note per set (client)"
65+
}
66+
EXERCISE_LOGS {
67+
relationship session
68+
relationship client
69+
relationship exercise
70+
text exerciseName
71+
relationship exerciseRow
72+
relationship roundLog
73+
textarea note "LOG note per exercise (client) — NEW"
74+
}
75+
```
76+
77+
### Diagram — save flow (upsert in the tracker)
78+
79+
```mermaid
80+
sequenceDiagram
81+
actor C as Client
82+
participant EC as ExerciseCard
83+
participant H as useWorkoutSession
84+
participant API as Payload (sdk)
85+
86+
C->>EC: types the exercise note
87+
EC->>H: saveExerciseNote(ex, note)
88+
H->>H: ensureSession()
89+
alt log for (session, exerciseRow) exists
90+
H->>API: sdk.update exercise-logs
91+
else no log yet
92+
H->>API: sdk.create exercise-logs
93+
end
94+
API-->>H: doc
95+
H->>H: update exerciseNotes state
96+
H-->>EC: noteForRow(rowId) → render
97+
```
98+
99+
---
100+
101+
## Architecture
102+
103+
Layers: **Collection (model + access + hooks) → SDK (mutation in a React hook) → UI component**. No custom endpoints — we use Payload's REST/SDK (same as `set-logs`).
104+
105+
### File Structure
106+
107+
```
108+
src/
109+
├── collections/
110+
│ ├── exercise-logs/
111+
│ │ └── index.ts # NEW collection
112+
│ └── index.ts # + re-export ExerciseLogs
113+
├── payload.config.ts # + register in collections[]
114+
├── components/workout/
115+
│ ├── workout-tracker/
116+
│ │ ├── workout-tracker.tsx # pass note + onSaveNote to the card
117+
│ │ └── hooks/
118+
│ │ └── use-workout-session.ts # load + noteForRow + saveExerciseNote
119+
│ └── exercise-card/
120+
│ ├── exercise-card.tsx # wires in the note (header) + onSaveNote
121+
│ └── components/exercise-note/ # NEW: display + inline edit subcomponent
122+
├── scripts/export-seed.ts # comment note (logs already skipped via allow-list)
123+
└── migrations/
124+
├── 20260620_195052_exercise_logs.ts # create exercise_logs table
125+
└── 20260620_200543_exercise_logs_relations.ts # + exercise_name, round_log_id
126+
messages/
127+
├── pl.json # UI strings
128+
└── en.json
129+
```
130+
131+
---
132+
133+
## Data Models
134+
135+
### `exercise-logs` collection
136+
137+
Full relational parity with `set-logs` (so the collection can grow beyond a note later):
138+
139+
| Field | Type | Required | Notes |
140+
|---|---|---|---|
141+
| `session` | relationship → `workout-logs` | yes | training session |
142+
| `client` | relationship → `clients` | no | `defaultValue` from user (as in set-logs) |
143+
| `exercise` | relationship → `exercises` | no | catalog snapshot (reporting) |
144+
| `exerciseName` | text | no | name snapshot (as in set-logs) |
145+
| `exerciseRow` | relationship → `workout-exercise-rows` | yes | exercise key |
146+
| `roundLog` | relationship → `round-logs` | no | `admin.readOnly`; reserved (round-logs not yet written) |
147+
| `note` | textarea | no | note body |
148+
149+
**Uniqueness `(session, exerciseRow)`** — one note per exercise per session. Enforced via the upsert pattern in the UI (find → update / create); optionally hardened with a `beforeValidate` hook that rejects duplicates.
150+
151+
**Access** (copy of the [set-logs](../../src/collections/set-logs/index.ts) pattern):
152+
153+
| Operation | Rule |
154+
|---|---|
155+
| create | `({ req:{user} }) => Boolean(user)` |
156+
| read | `adminOrOwnByClient` → fallback `canReadViaShareToken` |
157+
| update | `adminOrOwnByClient` |
158+
| delete | `adminOrOwnByClient` |
159+
160+
**Hooks:**
161+
- `beforeChange`: for `user.collection === 'clients'` set `data.client = req.user.id` (ID from the user session, never from the request).
162+
- `beforeValidate`: validate `session` ownership (a client cannot log to someone else's session) — analogous to [set-logs.beforeValidate](../../src/collections/set-logs/index.ts).
163+
164+
**Admin:** `group: 'Training log'`, `useAsTitle: 'id'`, `defaultColumns: ['exerciseRow', 'session', 'client']`.
165+
166+
---
167+
168+
## API Contracts
169+
170+
No custom routes. Operations go through the Payload SDK (`@/lib/sdk`) on the `exercise-logs` collection:
171+
172+
```
173+
sdk.find({ collection: 'exercise-logs', where: { session: { equals: sessionId } } })
174+
sdk.create({ collection: 'exercise-logs', data: { session, exercise?, exerciseName, exerciseRow, note } })
175+
sdk.update({ collection: 'exercise-logs', id, data: { note } })
176+
```
177+
178+
Access control and the `client` assignment are enforced by the collection's access rules + hooks (not by the UI layer).
179+
180+
---
181+
182+
## Workflow Design
183+
184+
```
185+
useWorkoutSession (hook)
186+
├── [load] sdk.find workout-logs (session) — exists
187+
├── [load] sdk.find set-logs + exercise-logs — Promise.all; exercise-logs → exerciseNotes state (NEW)
188+
├── [select] noteForRow(rowId) — NEW (analogous to setsForRow)
189+
└── [mutate] saveExerciseNote(ex, note) — NEW
190+
├── ensureSession()
191+
├── upsert: update if exists, otherwise create
192+
└── update local state
193+
```
194+
195+
UI: [workout-tracker.tsx](../../src/components/workout/workout-tracker/workout-tracker.tsx) passes `note` + `onSaveNote` to [exercise-card.tsx](../../src/components/workout/exercise-card/exercise-card.tsx), which renders the note in the exercise header (distinct from the plan note) and — when `!readOnly` — exposes an editable field.
196+
197+
---
198+
199+
## Phasing
200+
201+
### Phase 1 — Backend (collection + migrations)
202+
The `exercise-logs` collection, registration, access/hooks, two migrations (table + relations), types. **Deliverable:** the exercise note can be created/edited via the admin and the SDK; typecheck/lint pass.
203+
204+
### Phase 2 — Runtime (session hook)
205+
Loading `exercise-logs`, `noteForRow`, `saveExerciseNote` (upsert). **Deliverable:** note data is available in the tracker (logic ready, no UI yet).
206+
207+
### Phase 3 — UI + i18n
208+
Render and edit the note in `ExerciseCard`, strings in `pl.json`/`en.json`, visibility in read-only/share mode. **Deliverable:** a client can add/edit the exercise note in the app.
209+
210+
---
211+
212+
## Implementation Plan
213+
214+
### Phase 1 — Backend
215+
- [x] Create `src/collections/exercise-logs/index.ts` (fields, access, hooks per the Data Models section).
216+
- [x] Re-export in [src/collections/index.ts](../../src/collections/index.ts).
217+
- [x] Register in `collections[]` in [src/payload.config.ts](../../src/payload.config.ts).
218+
- [x] `yarn generate:types``ExerciseLog` type.
219+
- [x] `yarn payload migrate:create` → two migrations (`..._exercise_logs` table; `..._exercise_logs_relations` adds `exercise_name` + `round_log_id`).
220+
- [x] `yarn payload migrate` — run by the user (DB-mutating step).
221+
222+
### Phase 2 — Runtime
223+
- [x] [use-workout-session.ts](../../src/components/workout/workout-tracker/hooks/use-workout-session.ts): load `exercise-logs` by `session` (parallel with set-logs), add `exerciseNotes` state.
224+
- [x] Add the `noteForRow(rowId)` selector and the `saveExerciseNote(ex, note)` mutation (upsert).
225+
- [x] Expose both in the hook's returned API.
226+
227+
### Phase 3 — UI + i18n
228+
- [x] [workout-tracker.tsx](../../src/components/workout/workout-tracker/workout-tracker.tsx): pass `clientNote` + `onSaveNote` to `ExerciseCard`.
229+
- [x] [exercise-card.tsx](../../src/components/workout/exercise-card/exercise-card.tsx): render the client note in the header (new `exercise-note` subcomponent) + allow editing when `!readOnly`.
230+
- [x] Strings (`addNote`, `notePlaceholder`, `saveNote`, `cancelNote`) in `messages/pl.json` + `messages/en.json`.
231+
- [x] [export-seed.ts](../../src/scripts/export-seed.ts): comment updated (logs already excluded via the allow-list).
232+
- [x] Typecheck (`tsc --noEmit`) + lint pass.
233+
234+
---
235+
236+
## Risks & Impact
237+
238+
| Risk | Severity | Mitigation |
239+
|------|----------|-----------|
240+
| Duplicate notes for `(session, exerciseRow)` on a race condition | medium | Upsert in the UI + optional `beforeValidate` hook rejecting duplicates |
241+
| Notes leaking between clients | high | `adminOrOwnByClient` access; `client` set in `beforeChange` from `req.user.id`, never from the request |
242+
| Log note confused with the plan note in the UI | low | Visual distinction in `ExerciseCard` (separate style/label) |
243+
| Notes missing in the share view | medium | `read` with `canReadViaShareToken` fallback; verify in read-only mode |
244+
245+
---
246+
247+
## Compliance Review — Payload CMS / Next.js
248+
249+
| Rule | Status | Notes |
250+
|------|--------|-------|
251+
| New collection registered in `payload.config.ts` and exported from `collections/index.ts` || Phase 1 |
252+
| Migration generated (`migrate:create`) and applied (`migrate`) || Two migrations generated; `migrate` run by the user |
253+
| `yarn generate:types` after schema change || Phase 1 |
254+
| `overrideAccess: true` only in server-side loaders, never in client handlers || Tracker uses `sdk` with the logged-in client's permissions — no `overrideAccess` |
255+
| Sensitive IDs (client) sourced from the DB doc, not the request || `client` set in `beforeChange` from `req.user.id` |
256+
| All 4 access operations (create/read/update/delete) defined || Access section |
257+
| Custom admin components in importMap (`generate:importmap`) | N/A | No new admin components |
258+
| `yarn build` after each phase || Covered in the plan |
259+
| User-facing strings only via i18n (`pl.json` + `en.json`) || Phase 3 |
260+
261+
**Verdict:** APPROVED — all rules pass or are N/A with justification.
262+
263+
---
264+
265+
## Changelog
266+
267+
| Date | Author | Change |
268+
|------|--------|--------|
269+
| 2026-06-20 | Krzysztof Polak | Initial draft (Option B — `exercise-logs` collection) |
270+
| 2026-06-22 | Krzysztof Polak | Implemented. Collection given full relational parity with `set-logs` (added `exercise`, `exerciseName`, `roundLog`); split into two migrations (table + relations); `migrate` left to the user. Status → implemented. |

AGENTS.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,14 @@ yarn seed # seed demo data
130130
yarn lint # ESLint
131131
npx skills add <src> -a claude-code -a codex --copy # install skills
132132
```
133+
134+
---
135+
136+
## Release
137+
138+
Releases use [Changesets](https://github.com/changesets/changesets).
139+
140+
- For any user-facing change, add a changeset: `yarn changeset` (or create a `.changeset/*.md` file with the bump level and summary).
141+
- **The maintainer runs the release flow**`yarn prepare-release` (runs `changeset version`, commits the bump, pushes `develop`, creates the `release/vX.Y.Z` branch, and opens the release PR).
142+
- Agents must **not** run `prepare-release` or `git push` to remote. Prepare locally only — create the changeset and commits — then hand off with the exact command (`yarn prepare-release`). Push/publish only if the maintainer explicitly asks this time.
143+
- Branch model and the full human release process: see `CONTRIBUTING.md`.

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# training-app
22

3+
## 1.3.0
4+
5+
### Minor Changes
6+
7+
- 53b1f57: Workout tracker: client notes and colored blocks
8+
9+
- Exercise client note: unified add/edit UI (`NoteField`) with lucide icons (`Plus`/`Pencil`), label-prefixed display, moved to the bottom of the exercise card.
10+
- Workout note: the per-session note (`workout-logs.notes`) is now editable in the tracker footer; relabeled from "session" to "workout" to avoid ambiguity.
11+
- Colored blocks: groups can be merged into one colored band via the new `bundleWithPrevious` field on `workout-groups`. The loader bundles consecutive groups into `blocks` (index resets per section); the tracker renders one background per block.
12+
- Workout ID is shown in the tracker header for identification.
13+
314
## 1.2.0
415

516
### Minor Changes

CONTRIBUTING.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ This guide explains how we organize releases, structure branches, and prepare pu
1010

1111
## Working on Features
1212

13+
> **External contributors:** your flow ends at a PR to `develop` — branch from `develop`, add a changeset, open the PR. You do **not** create releases; maintainers handle the [Release Process](#release-process) below.
14+
1315
1. **Branch from `develop`**:
1416
```bash
1517
git checkout develop
@@ -34,6 +36,8 @@ This guide explains how we organize releases, structure branches, and prepare pu
3436

3537
## Release Process
3638

39+
> **Maintainers only.** Steps 1–3 are automated by `yarn prepare-release` (runs `changeset version`, commits, pushes `develop`, creates the `release/vX.Y.Z` branch, and opens the release PR). The steps below document what the script does.
40+
3741
### Normal Release Flow
3842

3943
1. **Prepare release on `develop`**:

0 commit comments

Comments
 (0)