Add editable TUI workflows and unified navigation
This commit is contained in:
parent
23c5fcdf06
commit
3d99be00f5
12 changed files with 6556 additions and 853 deletions
32
CHANGELOG.md
32
CHANGELOG.md
|
|
@ -91,3 +91,35 @@ Next:
|
||||||
- added transactional SQLite schema migration v1 -> v2;
|
- added transactional SQLite schema migration v1 -> v2;
|
||||||
- preserved migrated sessions as `training`;
|
- preserved migrated sessions as `training`;
|
||||||
- kept Trainlog JSON v1 frozen and unchanged.
|
- kept Trainlog JSON v1 frozen and unchanged.
|
||||||
|
|
||||||
|
### Transaction-safe session editing foundation
|
||||||
|
|
||||||
|
- added exact bounded loading of persisted exercise/set values;
|
||||||
|
- added atomic replacement of recorded session exercise/set rows;
|
||||||
|
- preserved the parent session row and linked body observations;
|
||||||
|
- added rollback coverage for failed replacements.
|
||||||
|
|
||||||
|
### Body observation history and correction
|
||||||
|
|
||||||
|
- redesigned `F4 Corps` around recorded measurement dates;
|
||||||
|
- added a framed trend graph above the newest-first record list;
|
||||||
|
- added PageUp/PageDown and a visual ncurses scrollbar;
|
||||||
|
- added two-page detail views with `e Modifier` on every page;
|
||||||
|
- added correction of existing observations without changing their timestamp or
|
||||||
|
session link;
|
||||||
|
- added `-` to clear one erroneous measurement and Escape to cancel the edit.
|
||||||
|
|
||||||
|
<!-- TRAINLOG_EDITABILITY_NAV_CHANGELOG -->
|
||||||
|
### TUI editability and navigation checkpoint
|
||||||
|
|
||||||
|
- added local `training` / `max_test` selection to the session workflow;
|
||||||
|
- added safe persisted-session editing while preserving parent session identity;
|
||||||
|
- added newest-first body-observation history, detail pages, and editing;
|
||||||
|
- added immediate Escape cancellation to text/numeric entry paths;
|
||||||
|
- added a shared top navigation bar with `0 Accueil`, `1-4`, and `F1-F4`;
|
||||||
|
- added Tab/Shift+Tab focus on multi-zone screens;
|
||||||
|
- focused frames use a yellow border/title without recoloring content;
|
||||||
|
- added consistent ASCII banners and ncurses frames to secondary detail views;
|
||||||
|
- added direct exercise creation from the in-session exercise chooser;
|
||||||
|
- kept the final rolling 12-month `MM/YY` dashboard label inside its frame.
|
||||||
|
<!-- TRAINLOG_EDITABILITY_NAV_CHANGELOG _END -->
|
||||||
|
|
|
||||||
38
docs/reviews/body_observation_edit.md
Normal file
38
docs/reviews/body_observation_edit.md
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
# Body observation history and editing
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
```text
|
||||||
|
BODY_OBSERVATION_HISTORY_UI=IMPLEMENTED
|
||||||
|
BODY_OBSERVATION_EDIT=IMPLEMENTED
|
||||||
|
BODY_OBSERVATION_SCROLLBAR=IMPLEMENTED
|
||||||
|
DATABASE_SCHEMA_V2=UNCHANGED
|
||||||
|
TRAINLOG_FORMAT_V1=FROZEN
|
||||||
|
```
|
||||||
|
|
||||||
|
`F4 Corps` is now record-oriented rather than metric-oriented.
|
||||||
|
|
||||||
|
The home view contains:
|
||||||
|
|
||||||
|
- a framed body-weight trend graph;
|
||||||
|
- a newest-first framed observation list;
|
||||||
|
- one compact summary per observation;
|
||||||
|
- keyboard scrolling plus PageUp/PageDown;
|
||||||
|
- a ncurses-drawn vertical scrollbar when history exceeds the visible window.
|
||||||
|
|
||||||
|
Enter opens the selected observation.
|
||||||
|
|
||||||
|
The detail view has two pages:
|
||||||
|
|
||||||
|
1. general measurements;
|
||||||
|
2. left/right limb measurements.
|
||||||
|
|
||||||
|
Both pages expose `e Modifier`.
|
||||||
|
|
||||||
|
Editing preserves the observation row identity, timestamp and optional session
|
||||||
|
link. A field can be kept with Enter, replaced with another positive number, or
|
||||||
|
cleared using `-`. Escape cancels the entire edit without persistence.
|
||||||
|
|
||||||
|
At least one metric must remain present.
|
||||||
|
|
||||||
|
No schema migration and no JSON v1 change are required.
|
||||||
33
docs/reviews/session_edit_persistence.md
Normal file
33
docs/reviews/session_edit_persistence.md
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Transaction-safe session editing
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
```text
|
||||||
|
SESSION_EDIT_PERSISTENCE=IMPLEMENTED
|
||||||
|
SESSION_EDIT_TUI=NEXT
|
||||||
|
TRAINLOG_FORMAT_V1=FROZEN
|
||||||
|
DATABASE_SCHEMA_V2=UNCHANGED
|
||||||
|
```
|
||||||
|
|
||||||
|
This slice adds the persistence foundation required to correct a recorded
|
||||||
|
workout without deleting the session itself.
|
||||||
|
|
||||||
|
`trainlog_database_replace_session_exercises(...)` replaces the
|
||||||
|
`session_exercises` and `performed_sets` children inside one explicit
|
||||||
|
transaction.
|
||||||
|
|
||||||
|
The parent `sessions` row is never deleted. Therefore these remain stable:
|
||||||
|
|
||||||
|
- `session_id`;
|
||||||
|
- start/end timestamps;
|
||||||
|
- `session_type`;
|
||||||
|
- session notes;
|
||||||
|
- any `body_observations.session_row_id` link.
|
||||||
|
|
||||||
|
On any lookup, constraint or insertion error, the whole replacement rolls back.
|
||||||
|
|
||||||
|
`trainlog_database_load_session_editable(...)` exposes the exact persisted
|
||||||
|
exercise/set values in bounded caller-owned buffers. It refuses insufficient
|
||||||
|
capacity instead of silently truncating editable data.
|
||||||
|
|
||||||
|
No schema migration and no JSON v1 change are required.
|
||||||
|
|
@ -33,14 +33,15 @@ Current TUI capabilities:
|
||||||
- navigable history;
|
- navigable history;
|
||||||
- Unicode anti-duplicate exercise names.
|
- Unicode anti-duplicate exercise names.
|
||||||
|
|
||||||
Next TUI polish blocks:
|
Current remaining Gate 2 direction:
|
||||||
|
|
||||||
1. full session detail;
|
1. finish the current visual/navigation checkpoint;
|
||||||
2. exercise performance history and graphs;
|
2. validate persisted editing on the real database;
|
||||||
3. previous-session defaults;
|
3. detect Android over USB/ADB;
|
||||||
4. safe edit/delete flows.
|
4. build the minimal Android recorder;
|
||||||
|
5. exercise frozen JSON v1 import/export end to end.
|
||||||
|
|
||||||
Android remains deferred until the TUI daily workflow is satisfactory.
|
Measured-max semantics remain a later independent analytics contract.
|
||||||
|
|
||||||
<!-- TRAINLOG_TUI_V02_ROADMAP -->
|
<!-- TRAINLOG_TUI_V02_ROADMAP -->
|
||||||
## TUI v0.2 checkpoint
|
## TUI v0.2 checkpoint
|
||||||
|
|
@ -130,11 +131,65 @@ PREVIOUS_SESSION_DEFAULTS=AFTER
|
||||||
```text
|
```text
|
||||||
DATABASE_SCHEMA_V2=IMPLEMENTED
|
DATABASE_SCHEMA_V2=IMPLEMENTED
|
||||||
SESSION_TYPE_PERSISTENCE=IMPLEMENTED
|
SESSION_TYPE_PERSISTENCE=IMPLEMENTED
|
||||||
SESSION_TYPE_TUI=NEXT
|
SESSION_TYPE_TUI=IMPLEMENTED
|
||||||
MEASURED_MAX_TRACKING=AFTER
|
TUI_SESSION_EDIT=IMPLEMENTED
|
||||||
|
TUI_BODY_OBSERVATION_EDIT=IMPLEMENTED
|
||||||
|
TUI_PRIMARY_NAVIGATION=IMPLEMENTED
|
||||||
|
TUI_SECONDARY_VIEW_POLISH=IMPLEMENTED
|
||||||
|
ANDROID_USB_DETECTION=NEXT
|
||||||
|
MEASURED_MAX_TRACKING=LATER
|
||||||
TRAINLOG_FORMAT_V1=FROZEN
|
TRAINLOG_FORMAT_V1=FROZEN
|
||||||
```
|
```
|
||||||
|
|
||||||
SQLite schema v2 adds `sessions.session_type` with `training` and `max_test`.
|
SQLite schema v2 adds `sessions.session_type` with `training` and `max_test`.
|
||||||
Existing v1 rows migrate to `training`. No existing session is retroactively
|
Existing v1 rows migrate to `training`. No existing session is retroactively
|
||||||
classified as a max test.
|
classified as a max test.
|
||||||
|
|
||||||
|
## Editable session data
|
||||||
|
|
||||||
|
```text
|
||||||
|
SESSION_EDIT_PERSISTENCE=IMPLEMENTED
|
||||||
|
SESSION_EDIT_TUI=NEXT
|
||||||
|
BODY_OBSERVATION_EDIT=AFTER
|
||||||
|
USB_PHONE_DETECTION=AFTER_EDITING
|
||||||
|
```
|
||||||
|
|
||||||
|
Recorded session exercise/set correction uses atomic replacement of child rows
|
||||||
|
while preserving the parent session row and linked body observations.
|
||||||
|
|
||||||
|
## Body observation record workflow
|
||||||
|
|
||||||
|
```text
|
||||||
|
BODY_OBSERVATION_HISTORY_UI=IMPLEMENTED
|
||||||
|
BODY_OBSERVATION_EDIT=IMPLEMENTED
|
||||||
|
BODY_OBSERVATION_SCROLLBAR=IMPLEMENTED
|
||||||
|
USB_PHONE_DETECTION=NEXT
|
||||||
|
```
|
||||||
|
|
||||||
|
`F4 Corps` now uses newest-first observation records with detail/edit views
|
||||||
|
instead of making individual metrics the primary navigation model.
|
||||||
|
|
||||||
|
<!-- TRAINLOG_EDITABILITY_ROADMAP -->
|
||||||
|
## Editability/navigation checkpoint
|
||||||
|
|
||||||
|
Completed:
|
||||||
|
|
||||||
|
- local schema v2 migration with `training` / `max_test`;
|
||||||
|
- session-type selection and persistence;
|
||||||
|
- persisted session editing with stable parent identity;
|
||||||
|
- body-observation history, detail, and editing;
|
||||||
|
- Escape-safe prompt cancellation;
|
||||||
|
- framed ASCII-banner primary and secondary views;
|
||||||
|
- top `Accueil / Séance / Historique / Exercices / Corps` navigation;
|
||||||
|
- Tab focus with yellow border-only focus indication;
|
||||||
|
- direct exercise creation while building a session;
|
||||||
|
- 12-month dashboard axis kept inside its frame.
|
||||||
|
|
||||||
|
Next implementation cursor:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ANDROID_USB_DETECTION=NEXT
|
||||||
|
ANDROID_MINIMAL_RECORDER=AFTER
|
||||||
|
JSON_V1_USB_IMPORT_EXPORT=AFTER
|
||||||
|
```
|
||||||
|
<!-- TRAINLOG_EDITABILITY_ROADMAP _END -->
|
||||||
|
|
|
||||||
|
|
@ -81,36 +81,53 @@ Gate 1 includes rejection of:
|
||||||
- blank exercise notes;
|
- blank exercise notes;
|
||||||
- negative actual repetitions.
|
- negative actual repetitions.
|
||||||
|
|
||||||
## 7. Future database validation
|
## 7. Gate 2 compiled validation
|
||||||
|
|
||||||
Gate 2 must verify:
|
The normal Meson suite currently covers:
|
||||||
|
|
||||||
- foreign keys enabled;
|
```text
|
||||||
- duplicate `session_id` idempotency;
|
database
|
||||||
- duplicate `exercise_id` barrier;
|
catalog
|
||||||
- transactional rollback;
|
session_detail
|
||||||
- schema migration correctness.
|
duration
|
||||||
|
body_metrics
|
||||||
|
bodyviz
|
||||||
|
exercise_performance
|
||||||
|
session_type_schema
|
||||||
|
session_edit
|
||||||
|
body_observation_edit
|
||||||
|
```
|
||||||
|
|
||||||
## 8. Future C validation
|
The session-edit test verifies transactional child replacement without changing
|
||||||
|
the parent session identity. The body-observation edit test verifies stable
|
||||||
|
observation identity while metric values and notes are updated.
|
||||||
|
|
||||||
C implementation gates will include:
|
Schema validation includes the v1 -> v2 `session_type` migration.
|
||||||
|
|
||||||
- normal build;
|
## 8. C validation
|
||||||
- strict-warning build;
|
|
||||||
- ASan/UBSan build;
|
Current pre-push validation includes:
|
||||||
- formatter check;
|
|
||||||
- relevant unit/integration tests.
|
- normal strict-warning build;
|
||||||
|
- the complete Meson test suite;
|
||||||
|
- `git diff --check`;
|
||||||
|
- frozen JSON v1 validators.
|
||||||
|
|
||||||
|
ASan/UBSan is run for meaningful implementation checkpoints before declaring a
|
||||||
|
gate complete.
|
||||||
|
|
||||||
## 9. Pre-push checklist
|
## 9. Pre-push checklist
|
||||||
|
|
||||||
Before every meaningful push:
|
Before every meaningful push:
|
||||||
|
|
||||||
1. run `python tools/validate_json.py`;
|
1. run `python tools/validate_json.py`;
|
||||||
2. run relevant compiled tests when available;
|
2. run `python tools/validate_import_contract.py`;
|
||||||
3. run sanitizers when relevant;
|
3. run `meson compile -C build`;
|
||||||
4. run `git diff --check`;
|
4. run `meson test -C build --print-errorlogs`;
|
||||||
5. inspect `git status --short`;
|
5. run sanitizers when relevant;
|
||||||
6. review documentation changes.
|
6. run `git diff --check`;
|
||||||
|
7. inspect `git status --short`;
|
||||||
|
8. review documentation changes.
|
||||||
## 10. Catalog reconciliation contract
|
## 10. Catalog reconciliation contract
|
||||||
|
|
||||||
Before the C17 importer exists, Gate 1 defines local catalog merge behavior through an executable Python specification.
|
Before the C17 importer exists, Gate 1 defines local catalog merge behavior through an executable Python specification.
|
||||||
|
|
|
||||||
169
docs/tui.md
169
docs/tui.md
|
|
@ -232,7 +232,7 @@ value instead of repeating the same minimum and maximum label.
|
||||||
|
|
||||||
History is navigable with the keyboard.
|
History is navigable with the keyboard.
|
||||||
|
|
||||||
Selecting a workout and pressing `Enter` opens a read-only detail view.
|
Selecting a workout and pressing `Enter` opens its detail view.
|
||||||
|
|
||||||
For every exercise the detail view exposes:
|
For every exercise the detail view exposes:
|
||||||
|
|
||||||
|
|
@ -262,7 +262,7 @@ Réalisé:
|
||||||
5@80.0 / 5@80.0 / 5@80.0 / 3@80.0
|
5@80.0 / 5@80.0 / 5@80.0 / 3@80.0
|
||||||
```
|
```
|
||||||
|
|
||||||
Inside one workout, left/right or up/down changes the selected exercise.
|
Inside one workout, left/right or up/down changes the selected exercise. `e` opens the persisted session editor without replacing the parent session identity or timestamps.
|
||||||
|
|
||||||
Assistance remains direction-aware: more assistance kilograms mean more help,
|
Assistance remains direction-aware: more assistance kilograms mean more help,
|
||||||
not greater strength.
|
not greater strength.
|
||||||
|
|
@ -313,11 +313,42 @@ The same parser and formatter are reused for:
|
||||||
|
|
||||||
Invalid malformed forms are rejected before persistence.
|
Invalid malformed forms are rejected before persistence.
|
||||||
|
|
||||||
## 19. Body screen and measurement graphs — implemented
|
## 19. Body screen and measurement history — implemented
|
||||||
|
|
||||||
`F4 Corps` becomes a full history and visualization screen.
|
`F4 Corps` is record-oriented.
|
||||||
|
|
||||||
Canonical selectable metrics:
|
The primary body screen shows:
|
||||||
|
|
||||||
|
- the weight evolution graph;
|
||||||
|
- recorded body observations newest first;
|
||||||
|
- a compact summary per observation;
|
||||||
|
- a visual scrollbar when the history is longer than the visible area.
|
||||||
|
|
||||||
|
Controls:
|
||||||
|
|
||||||
|
```text
|
||||||
|
↑↓ / PgUp / PgDn select a recorded observation
|
||||||
|
Enter open observation detail
|
||||||
|
e edit the selected observation
|
||||||
|
a add an observation
|
||||||
|
g open the normalized global overlay
|
||||||
|
b / Esc return
|
||||||
|
```
|
||||||
|
|
||||||
|
Each observation detail is split into two framed pages:
|
||||||
|
|
||||||
|
```text
|
||||||
|
GENERAL
|
||||||
|
MEMBRES
|
||||||
|
```
|
||||||
|
|
||||||
|
Left/right changes page and `e` edits the observation.
|
||||||
|
|
||||||
|
Editing preserves the observation identity, timestamp, and optional session
|
||||||
|
link. Enter keeps the existing value, `-` clears a metric, and Escape cancels
|
||||||
|
the complete edit without persistence.
|
||||||
|
|
||||||
|
Canonical persisted metrics remain:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
body_weight_kg
|
body_weight_kg
|
||||||
|
|
@ -336,57 +367,26 @@ left_calf_cm
|
||||||
right_calf_cm
|
right_calf_cm
|
||||||
```
|
```
|
||||||
|
|
||||||
The screen supports left/right navigation between metrics.
|
Missing observations are never invented as zero values.
|
||||||
|
|
||||||
For the selected metric it shows:
|
## 20. Current priority before Android
|
||||||
|
|
||||||
- latest value;
|
The TUI editability checkpoint is complete enough to move toward the Android
|
||||||
- first recorded value;
|
input workflow.
|
||||||
- absolute change;
|
|
||||||
- terminal-native history graph;
|
|
||||||
- recent dated values.
|
|
||||||
|
|
||||||
Units:
|
Current order:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
body weight -> kg
|
1. finish visual/navigation consistency
|
||||||
measurements -> cm
|
2. keep persisted session/body editing safe
|
||||||
|
3. detect an Android phone over USB/ADB
|
||||||
|
4. build the minimal Android recorder
|
||||||
|
5. transfer/import through the frozen Trainlog JSON v1 contract
|
||||||
```
|
```
|
||||||
|
|
||||||
Paired measurements also expose asymmetry:
|
Measured-max analytics remain separate from ordinary best-set performance and
|
||||||
|
are not required for the Android transport milestone.
|
||||||
|
|
||||||
```text
|
|
||||||
left arm : 34.2 cm
|
|
||||||
right arm : 34.8 cm
|
|
||||||
difference: 0.6 cm right
|
|
||||||
```
|
|
||||||
|
|
||||||
Relevant pairs:
|
|
||||||
|
|
||||||
- arm;
|
|
||||||
- forearm;
|
|
||||||
- thigh;
|
|
||||||
- calf.
|
|
||||||
|
|
||||||
The graph layer must not invent zero values when one side or one date is
|
|
||||||
missing.
|
|
||||||
|
|
||||||
## 20. TUI priority before Android
|
|
||||||
|
|
||||||
Android remains intentionally deferred until the TUI is comfortable for daily
|
|
||||||
use.
|
|
||||||
|
|
||||||
Immediate order:
|
|
||||||
|
|
||||||
```text
|
|
||||||
1. session details
|
|
||||||
2. human duration parsing/formatting
|
|
||||||
3. full F4 measurement history and graphs
|
|
||||||
4. exercise performance history and graphs
|
|
||||||
5. previous-session defaults
|
|
||||||
6. safe editing/deletion
|
|
||||||
7. Android recorder and JSON import workflow
|
|
||||||
```
|
|
||||||
<!-- TRAINLOG_TUI_V02_CURRENT_AND_NEXT _END -->
|
<!-- TRAINLOG_TUI_V02_CURRENT_AND_NEXT _END -->
|
||||||
|
|
||||||
<!-- TRAINLOG_GLOBAL_BODY_GRAPH_NEXT -->
|
<!-- TRAINLOG_GLOBAL_BODY_GRAPH_NEXT -->
|
||||||
|
|
@ -422,8 +422,7 @@ Rules:
|
||||||
- normalization is display-only;
|
- normalization is display-only;
|
||||||
- canonical SQLite values remain untouched.
|
- canonical SQLite values remain untouched.
|
||||||
|
|
||||||
The global view is toggled from `F4 Corps` with `g`.
|
The global view is toggled from `F4 Corps` with `g`. The normal `F4 Corps` screen remains a newest-first observation history; left/right navigation is used inside observation detail pages.
|
||||||
Per-metric navigation remains left/right.
|
|
||||||
|
|
||||||
The global view also shows a compact percentage summary from first to latest
|
The global view also shows a compact percentage summary from first to latest
|
||||||
recorded value for each available metric.
|
recorded value for each available metric.
|
||||||
|
|
@ -445,17 +444,27 @@ It will show:
|
||||||
The dashboard remains intentionally compact.
|
The dashboard remains intentionally compact.
|
||||||
The complete multi-metric overlay belongs to `F4 Corps`.
|
The complete multi-metric overlay belongs to `F4 Corps`.
|
||||||
|
|
||||||
## 23. Immediate TUI implementation order
|
## 23. Current implementation cursor
|
||||||
|
|
||||||
```text
|
```text
|
||||||
TUI_DURATION_HUMAN_INPUT=IMPLEMENTED
|
TUI_DURATION_HUMAN_INPUT=IMPLEMENTED
|
||||||
TUI_BODY_METRIC_GRAPHS=IMPLEMENTED
|
TUI_BODY_METRIC_GRAPHS=IMPLEMENTED
|
||||||
TUI_GLOBAL_BODY_OVERLAY=NEXT
|
TUI_GLOBAL_BODY_OVERLAY=IMPLEMENTED
|
||||||
DASHBOARD_GRAPH_V2=NEXT
|
DASHBOARD_GRAPH_V2=IMPLEMENTED
|
||||||
EXERCISE_PERFORMANCE_GRAPHS=AFTER
|
DASHBOARD_12_MONTHS=IMPLEMENTED
|
||||||
|
TUI_EXERCISE_PERFORMANCE=IMPLEMENTED
|
||||||
|
DATABASE_SCHEMA_V2=IMPLEMENTED
|
||||||
|
SESSION_TYPE_PERSISTENCE=IMPLEMENTED
|
||||||
|
SESSION_TYPE_TUI=IMPLEMENTED
|
||||||
|
TUI_SESSION_EDIT=IMPLEMENTED
|
||||||
|
TUI_BODY_OBSERVATION_EDIT=IMPLEMENTED
|
||||||
|
TUI_PRIMARY_NAVIGATION=IMPLEMENTED
|
||||||
|
TUI_SECONDARY_VIEW_POLISH=IMPLEMENTED
|
||||||
|
ANDROID_USB_DETECTION=NEXT
|
||||||
```
|
```
|
||||||
|
|
||||||
Android remains deferred until these TUI daily-use views are satisfactory.
|
The frozen Trainlog JSON v1 contract remains unchanged.
|
||||||
|
|
||||||
<!-- TRAINLOG_GLOBAL_BODY_GRAPH_NEXT _END -->
|
<!-- TRAINLOG_GLOBAL_BODY_GRAPH_NEXT _END -->
|
||||||
|
|
||||||
## Dashboard graph-only layout
|
## Dashboard graph-only layout
|
||||||
|
|
@ -524,3 +533,53 @@ A recorded best set is not a measured maximum.
|
||||||
|
|
||||||
Measured maxima, max-session scheduling and working-load percentages remain a
|
Measured maxima, max-session scheduling and working-load percentages remain a
|
||||||
separate later contract.
|
separate later contract.
|
||||||
|
|
||||||
|
<!-- TRAINLOG_TUI_EDITABILITY_NAV_CHECKPOINT -->
|
||||||
|
## Current navigation and editability checkpoint
|
||||||
|
|
||||||
|
Primary large-layout screens share the same visual identity:
|
||||||
|
|
||||||
|
```text
|
||||||
|
TRAINLOG ASCII banner
|
||||||
|
top navigation bar
|
||||||
|
framed page content
|
||||||
|
footer shortcuts
|
||||||
|
```
|
||||||
|
|
||||||
|
The top navigation is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
0 Accueil 1 Séance 2 Historique 3 Exercices 4 Corps
|
||||||
|
```
|
||||||
|
|
||||||
|
Direct shortcuts keep `1`-`4` / `F1`-`F4`; `0` or `Home` returns to the
|
||||||
|
dashboard. On multi-zone pages, `Tab` / `Shift+Tab` changes focus. Only the
|
||||||
|
border and title of the focused frame use the warning/yellow role; content
|
||||||
|
colors are unchanged.
|
||||||
|
|
||||||
|
Large-layout framed/bannnered views include:
|
||||||
|
|
||||||
|
- dashboard;
|
||||||
|
- history;
|
||||||
|
- exercise catalog;
|
||||||
|
- body history;
|
||||||
|
- new-session type selection;
|
||||||
|
- in-progress session review;
|
||||||
|
- session detail;
|
||||||
|
- exercise performance;
|
||||||
|
- body-observation detail;
|
||||||
|
- exercise selection during session entry.
|
||||||
|
|
||||||
|
Session entry can create a missing exercise directly from the exercise chooser
|
||||||
|
with `a`, then return to the chooser.
|
||||||
|
|
||||||
|
Text prompts treat Escape as immediate cancellation. A cancelled draft or edit
|
||||||
|
does not persist partial state.
|
||||||
|
|
||||||
|
Persisted session replacement edits only session children. The parent session
|
||||||
|
row, stable ID, timestamps, session type, notes, and body-observation links are
|
||||||
|
preserved.
|
||||||
|
|
||||||
|
The dashboard rolling 12-month axis clamps the final `MM/YY` label inside the
|
||||||
|
dashboard frame so the current-month label does not overwrite the right border.
|
||||||
|
<!-- TRAINLOG_TUI_EDITABILITY_NAV_CHECKPOINT _END -->
|
||||||
|
|
|
||||||
|
|
@ -207,4 +207,121 @@ TrainlogStatus trainlog_database_list_exercise_performance(
|
||||||
size_t *output_count
|
size_t *output_count
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
/* TRAINLOG_SESSION_EDIT_API */
|
||||||
|
|
||||||
|
typedef struct TrainlogEditableExerciseRecord {
|
||||||
|
char exercise_id[TRAINLOG_ID_MAX + 1U];
|
||||||
|
char name[TRAINLOG_NAME_MAX + 1U];
|
||||||
|
TrainlogTrackingMode tracking_mode;
|
||||||
|
TrainlogLoadMode load_mode;
|
||||||
|
int rest_seconds;
|
||||||
|
int target_sets;
|
||||||
|
int target_reps;
|
||||||
|
int target_duration_seconds;
|
||||||
|
int has_target_weight;
|
||||||
|
double target_weight_kg;
|
||||||
|
char notes[TRAINLOG_NOTE_MAX + 1U];
|
||||||
|
size_t set_offset;
|
||||||
|
size_t set_count;
|
||||||
|
} TrainlogEditableExerciseRecord;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Load a complete persisted session into caller-owned editable buffers.
|
||||||
|
*
|
||||||
|
* The function never truncates a session silently. If either caller capacity is
|
||||||
|
* too small it returns TRAINLOG_STATUS_INVALID_ARGUMENT and the outputs must not
|
||||||
|
* be used.
|
||||||
|
*/
|
||||||
|
TrainlogStatus trainlog_database_load_session_editable(
|
||||||
|
TrainlogDatabase *database,
|
||||||
|
const char *session_id,
|
||||||
|
TrainlogSessionSummary *output_session,
|
||||||
|
TrainlogEditableExerciseRecord *output_exercises,
|
||||||
|
size_t exercise_capacity,
|
||||||
|
size_t *output_exercise_count,
|
||||||
|
TrainlogSetInput *output_sets,
|
||||||
|
size_t set_capacity,
|
||||||
|
size_t *output_set_count
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Replace only the exercise/set contents of an existing session.
|
||||||
|
*
|
||||||
|
* The session row itself is preserved, so session_id, timestamps, session type,
|
||||||
|
* session notes and body_observations.session_row_id remain attached to the
|
||||||
|
* same row. The replacement is atomic: any error rolls the whole operation
|
||||||
|
* back.
|
||||||
|
*/
|
||||||
|
TrainlogStatus trainlog_database_replace_session_exercises(
|
||||||
|
TrainlogDatabase *database,
|
||||||
|
const char *session_id,
|
||||||
|
const TrainlogSessionExerciseInput *exercises,
|
||||||
|
size_t exercise_count
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
/* TRAINLOG_BODY_OBSERVATION_RECORD_API */
|
||||||
|
|
||||||
|
typedef struct TrainlogBodyObservationRecord {
|
||||||
|
char observation_id[TRAINLOG_ID_MAX + 1U];
|
||||||
|
char observed_at[TRAINLOG_TIMESTAMP_MAX + 1U];
|
||||||
|
char session_id[TRAINLOG_ID_MAX + 1U];
|
||||||
|
|
||||||
|
bool has_body_weight;
|
||||||
|
double body_weight_kg;
|
||||||
|
bool has_neck;
|
||||||
|
double neck_cm;
|
||||||
|
bool has_shoulders;
|
||||||
|
double shoulders_cm;
|
||||||
|
bool has_chest;
|
||||||
|
double chest_cm;
|
||||||
|
bool has_waist;
|
||||||
|
double waist_cm;
|
||||||
|
bool has_hips;
|
||||||
|
double hips_cm;
|
||||||
|
bool has_left_arm;
|
||||||
|
double left_arm_cm;
|
||||||
|
bool has_right_arm;
|
||||||
|
double right_arm_cm;
|
||||||
|
bool has_left_forearm;
|
||||||
|
double left_forearm_cm;
|
||||||
|
bool has_right_forearm;
|
||||||
|
double right_forearm_cm;
|
||||||
|
bool has_left_thigh;
|
||||||
|
double left_thigh_cm;
|
||||||
|
bool has_right_thigh;
|
||||||
|
double right_thigh_cm;
|
||||||
|
bool has_left_calf;
|
||||||
|
double left_calf_cm;
|
||||||
|
bool has_right_calf;
|
||||||
|
double right_calf_cm;
|
||||||
|
|
||||||
|
char notes[TRAINLOG_NOTE_MAX + 1U];
|
||||||
|
} TrainlogBodyObservationRecord;
|
||||||
|
|
||||||
|
TrainlogStatus trainlog_database_list_body_observations(
|
||||||
|
TrainlogDatabase *database,
|
||||||
|
TrainlogBodyObservationRecord *output,
|
||||||
|
size_t capacity,
|
||||||
|
size_t *output_count
|
||||||
|
);
|
||||||
|
|
||||||
|
TrainlogStatus trainlog_database_get_body_observation(
|
||||||
|
TrainlogDatabase *database,
|
||||||
|
const char *observation_id,
|
||||||
|
TrainlogBodyObservationRecord *output
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Correct metric values of one existing observation.
|
||||||
|
*
|
||||||
|
* observation_id identifies the stable row. observed_at and session linkage
|
||||||
|
* remain unchanged. At least one body metric must remain present.
|
||||||
|
*/
|
||||||
|
TrainlogStatus trainlog_database_update_body_observation(
|
||||||
|
TrainlogDatabase *database,
|
||||||
|
const TrainlogBodyObservationInput *observation
|
||||||
|
);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -159,3 +159,27 @@ test(
|
||||||
'session_type_schema',
|
'session_type_schema',
|
||||||
test_session_type_schema,
|
test_session_type_schema,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
test_session_edit = executable(
|
||||||
|
'test_session_edit',
|
||||||
|
'tests/test_session_edit.c',
|
||||||
|
dependencies: trainlog_core_dep,
|
||||||
|
c_args: strict_c_args,
|
||||||
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
'session_edit',
|
||||||
|
test_session_edit,
|
||||||
|
)
|
||||||
|
|
||||||
|
test_body_observation_edit = executable(
|
||||||
|
'test_body_observation_edit',
|
||||||
|
'tests/test_body_observation_edit.c',
|
||||||
|
dependencies: trainlog_core_dep,
|
||||||
|
c_args: strict_c_args,
|
||||||
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
'body_observation_edit',
|
||||||
|
test_body_observation_edit,
|
||||||
|
)
|
||||||
|
|
|
||||||
1283
tui/src/database.c
1283
tui/src/database.c
File diff suppressed because it is too large
Load diff
4637
tui/src/tui.c
4637
tui/src/tui.c
File diff suppressed because it is too large
Load diff
190
tui/tests/test_body_observation_edit.c
Normal file
190
tui/tests/test_body_observation_edit.c
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
/**
|
||||||
|
* @file test_body_observation_edit.c
|
||||||
|
* @brief Body observation list/detail/update persistence tests.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "trainlog/database.h"
|
||||||
|
|
||||||
|
#define CHECK(condition) \
|
||||||
|
do { \
|
||||||
|
if (!(condition)) { \
|
||||||
|
(void)fprintf( \
|
||||||
|
stderr, \
|
||||||
|
"CHECK failed at %s:%d: %s\n", \
|
||||||
|
__FILE__, \
|
||||||
|
__LINE__, \
|
||||||
|
#condition \
|
||||||
|
); \
|
||||||
|
return false; \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
static bool test_body_observation_edit(void)
|
||||||
|
{
|
||||||
|
TrainlogDatabase *database = NULL;
|
||||||
|
TrainlogBodyObservationInput first;
|
||||||
|
TrainlogBodyObservationInput second;
|
||||||
|
TrainlogBodyObservationRecord records[4];
|
||||||
|
TrainlogBodyObservationRecord loaded;
|
||||||
|
size_t count = 0U;
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_open(
|
||||||
|
":memory:",
|
||||||
|
&database
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)memset(&first, 0, sizeof(first));
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
first.observation_id,
|
||||||
|
sizeof(first.observation_id),
|
||||||
|
"%s",
|
||||||
|
"bo_first"
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
first.observed_at,
|
||||||
|
sizeof(first.observed_at),
|
||||||
|
"%s",
|
||||||
|
"2026-08-01T08:00:00+02:00"
|
||||||
|
);
|
||||||
|
|
||||||
|
first.has_body_weight = true;
|
||||||
|
first.body_weight_kg = 85.0;
|
||||||
|
first.has_waist = true;
|
||||||
|
first.waist_cm = 98.0;
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_insert_body_observation(
|
||||||
|
database,
|
||||||
|
&first
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)memset(&second, 0, sizeof(second));
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
second.observation_id,
|
||||||
|
sizeof(second.observation_id),
|
||||||
|
"%s",
|
||||||
|
"bo_second"
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
second.observed_at,
|
||||||
|
sizeof(second.observed_at),
|
||||||
|
"%s",
|
||||||
|
"2026-09-01T08:00:00+02:00"
|
||||||
|
);
|
||||||
|
|
||||||
|
second.has_body_weight = true;
|
||||||
|
second.body_weight_kg = 84.0;
|
||||||
|
second.has_waist = true;
|
||||||
|
second.waist_cm = 96.0;
|
||||||
|
second.has_chest = true;
|
||||||
|
second.chest_cm = 104.0;
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_insert_body_observation(
|
||||||
|
database,
|
||||||
|
&second
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_list_body_observations(
|
||||||
|
database,
|
||||||
|
records,
|
||||||
|
4U,
|
||||||
|
&count
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(count == 2U);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
strcmp(
|
||||||
|
records[0].observation_id,
|
||||||
|
"bo_second"
|
||||||
|
) == 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
strcmp(
|
||||||
|
records[1].observation_id,
|
||||||
|
"bo_first"
|
||||||
|
) == 0
|
||||||
|
);
|
||||||
|
|
||||||
|
second.body_weight_kg = 83.5;
|
||||||
|
second.has_waist = false;
|
||||||
|
second.waist_cm = 0.0;
|
||||||
|
second.has_chest = true;
|
||||||
|
second.chest_cm = 105.0;
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_update_body_observation(
|
||||||
|
database,
|
||||||
|
&second
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_get_body_observation(
|
||||||
|
database,
|
||||||
|
"bo_second",
|
||||||
|
&loaded
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
strcmp(
|
||||||
|
loaded.observed_at,
|
||||||
|
"2026-09-01T08:00:00+02:00"
|
||||||
|
) == 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(loaded.has_body_weight);
|
||||||
|
CHECK(loaded.body_weight_kg > 83.49);
|
||||||
|
CHECK(loaded.body_weight_kg < 83.51);
|
||||||
|
CHECK(!loaded.has_waist);
|
||||||
|
CHECK(loaded.has_chest);
|
||||||
|
CHECK(loaded.chest_cm > 104.99);
|
||||||
|
CHECK(loaded.chest_cm < 105.01);
|
||||||
|
|
||||||
|
(void)memset(&second, 0, sizeof(second));
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
second.observation_id,
|
||||||
|
sizeof(second.observation_id),
|
||||||
|
"%s",
|
||||||
|
"bo_second"
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_update_body_observation(
|
||||||
|
database,
|
||||||
|
&second
|
||||||
|
) == TRAINLOG_STATUS_INVALID_ARGUMENT
|
||||||
|
);
|
||||||
|
|
||||||
|
trainlog_database_close(database);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
CHECK(test_body_observation_edit());
|
||||||
|
|
||||||
|
(void)printf(
|
||||||
|
"PASS body_observation_edit\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
364
tui/tests/test_session_edit.c
Normal file
364
tui/tests/test_session_edit.c
Normal file
|
|
@ -0,0 +1,364 @@
|
||||||
|
/**
|
||||||
|
* @file test_session_edit.c
|
||||||
|
* @brief Transaction-safe persisted session editing tests.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "trainlog/database.h"
|
||||||
|
|
||||||
|
#define CHECK(condition) \
|
||||||
|
do { \
|
||||||
|
if (!(condition)) { \
|
||||||
|
(void)fprintf( \
|
||||||
|
stderr, \
|
||||||
|
"CHECK failed at %s:%d: %s\n", \
|
||||||
|
__FILE__, \
|
||||||
|
__LINE__, \
|
||||||
|
#condition \
|
||||||
|
); \
|
||||||
|
return false; \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
static bool add_exercises(
|
||||||
|
TrainlogDatabase *database
|
||||||
|
)
|
||||||
|
{
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_insert_exercise(
|
||||||
|
database,
|
||||||
|
"ex_press",
|
||||||
|
"Presse",
|
||||||
|
"presse",
|
||||||
|
TRAINLOG_TRACKING_REPS
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_insert_exercise(
|
||||||
|
database,
|
||||||
|
"ex_plank",
|
||||||
|
"Gainage",
|
||||||
|
"gainage",
|
||||||
|
TRAINLOG_TRACKING_DURATION
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void bind_reps_exercise(
|
||||||
|
TrainlogSessionExerciseInput *exercise,
|
||||||
|
TrainlogSetInput *sets,
|
||||||
|
size_t set_count,
|
||||||
|
double weight
|
||||||
|
)
|
||||||
|
{
|
||||||
|
size_t index;
|
||||||
|
|
||||||
|
(void)memset(
|
||||||
|
exercise,
|
||||||
|
0,
|
||||||
|
sizeof(*exercise)
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
exercise->exercise_id,
|
||||||
|
sizeof(exercise->exercise_id),
|
||||||
|
"%s",
|
||||||
|
"ex_press"
|
||||||
|
);
|
||||||
|
|
||||||
|
exercise->load_mode =
|
||||||
|
TRAINLOG_LOAD_EXTERNAL;
|
||||||
|
|
||||||
|
exercise->rest_seconds = 90;
|
||||||
|
exercise->target_sets = 3;
|
||||||
|
exercise->target_reps = 10;
|
||||||
|
exercise->target_has_weight = true;
|
||||||
|
exercise->target_weight_kg = weight;
|
||||||
|
exercise->notes = "note presse";
|
||||||
|
exercise->sets = sets;
|
||||||
|
exercise->set_count = set_count;
|
||||||
|
|
||||||
|
for (index = 0U;
|
||||||
|
index < set_count;
|
||||||
|
++index) {
|
||||||
|
(void)memset(
|
||||||
|
&sets[index],
|
||||||
|
0,
|
||||||
|
sizeof(sets[index])
|
||||||
|
);
|
||||||
|
|
||||||
|
sets[index].reps =
|
||||||
|
10 - (int)index;
|
||||||
|
|
||||||
|
sets[index].has_weight = true;
|
||||||
|
sets[index].weight_kg = weight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test_load_replace_and_rollback(void)
|
||||||
|
{
|
||||||
|
TrainlogDatabase *database = NULL;
|
||||||
|
TrainlogSetInput original_sets[3];
|
||||||
|
TrainlogSetInput replacement_sets[2];
|
||||||
|
TrainlogSetInput rollback_sets[1];
|
||||||
|
TrainlogSessionExerciseInput original;
|
||||||
|
TrainlogSessionExerciseInput replacement;
|
||||||
|
TrainlogSessionExerciseInput bad[2];
|
||||||
|
TrainlogSessionInput session;
|
||||||
|
TrainlogBodyObservationInput body;
|
||||||
|
|
||||||
|
TrainlogSessionSummary loaded_session;
|
||||||
|
TrainlogEditableExerciseRecord loaded_exercises[4];
|
||||||
|
TrainlogSetInput loaded_sets[16];
|
||||||
|
size_t exercise_count = 0U;
|
||||||
|
size_t set_count = 0U;
|
||||||
|
|
||||||
|
TrainlogBodyMetricPoint weight_points[4];
|
||||||
|
size_t weight_count = 0U;
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_open(
|
||||||
|
":memory:",
|
||||||
|
&database
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(add_exercises(database));
|
||||||
|
|
||||||
|
bind_reps_exercise(
|
||||||
|
&original,
|
||||||
|
original_sets,
|
||||||
|
3U,
|
||||||
|
80.0
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)memset(
|
||||||
|
&session,
|
||||||
|
0,
|
||||||
|
sizeof(session)
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
session.session_id,
|
||||||
|
sizeof(session.session_id),
|
||||||
|
"%s",
|
||||||
|
"se_edit"
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
session.started_at,
|
||||||
|
sizeof(session.started_at),
|
||||||
|
"%s",
|
||||||
|
"2026-09-06T08:00:00+02:00"
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
session.ended_at,
|
||||||
|
sizeof(session.ended_at),
|
||||||
|
"%s",
|
||||||
|
"2026-09-06T09:00:00+02:00"
|
||||||
|
);
|
||||||
|
|
||||||
|
session.session_type =
|
||||||
|
TRAINLOG_SESSION_TRAINING;
|
||||||
|
|
||||||
|
session.exercises = &original;
|
||||||
|
session.exercise_count = 1U;
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_insert_session(
|
||||||
|
database,
|
||||||
|
&session
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)memset(&body, 0, sizeof(body));
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
body.observation_id,
|
||||||
|
sizeof(body.observation_id),
|
||||||
|
"%s",
|
||||||
|
"bo_edit"
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
body.observed_at,
|
||||||
|
sizeof(body.observed_at),
|
||||||
|
"%s",
|
||||||
|
"2026-09-06T09:05:00+02:00"
|
||||||
|
);
|
||||||
|
|
||||||
|
body.session_id = "se_edit";
|
||||||
|
body.has_body_weight = true;
|
||||||
|
body.body_weight_kg = 84.1;
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_insert_body_observation(
|
||||||
|
database,
|
||||||
|
&body
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_load_session_editable(
|
||||||
|
database,
|
||||||
|
"se_edit",
|
||||||
|
&loaded_session,
|
||||||
|
loaded_exercises,
|
||||||
|
4U,
|
||||||
|
&exercise_count,
|
||||||
|
loaded_sets,
|
||||||
|
16U,
|
||||||
|
&set_count
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(exercise_count == 1U);
|
||||||
|
CHECK(set_count == 3U);
|
||||||
|
CHECK(
|
||||||
|
strcmp(
|
||||||
|
loaded_exercises[0].exercise_id,
|
||||||
|
"ex_press"
|
||||||
|
) == 0
|
||||||
|
);
|
||||||
|
CHECK(
|
||||||
|
strcmp(
|
||||||
|
loaded_exercises[0].notes,
|
||||||
|
"note presse"
|
||||||
|
) == 0
|
||||||
|
);
|
||||||
|
CHECK(loaded_exercises[0].set_offset == 0U);
|
||||||
|
CHECK(loaded_exercises[0].set_count == 3U);
|
||||||
|
CHECK(loaded_sets[0].reps == 10);
|
||||||
|
CHECK(loaded_sets[2].reps == 8);
|
||||||
|
CHECK(loaded_sets[0].weight_kg > 79.99);
|
||||||
|
CHECK(loaded_sets[0].weight_kg < 80.01);
|
||||||
|
|
||||||
|
bind_reps_exercise(
|
||||||
|
&replacement,
|
||||||
|
replacement_sets,
|
||||||
|
2U,
|
||||||
|
85.0
|
||||||
|
);
|
||||||
|
|
||||||
|
replacement_sets[0].reps = 12;
|
||||||
|
replacement_sets[1].reps = 11;
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_replace_session_exercises(
|
||||||
|
database,
|
||||||
|
"se_edit",
|
||||||
|
&replacement,
|
||||||
|
1U
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_load_session_editable(
|
||||||
|
database,
|
||||||
|
"se_edit",
|
||||||
|
&loaded_session,
|
||||||
|
loaded_exercises,
|
||||||
|
4U,
|
||||||
|
&exercise_count,
|
||||||
|
loaded_sets,
|
||||||
|
16U,
|
||||||
|
&set_count
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(exercise_count == 1U);
|
||||||
|
CHECK(set_count == 2U);
|
||||||
|
CHECK(loaded_sets[0].reps == 12);
|
||||||
|
CHECK(loaded_sets[1].reps == 11);
|
||||||
|
CHECK(loaded_sets[0].weight_kg > 84.99);
|
||||||
|
CHECK(loaded_sets[0].weight_kg < 85.01);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The body observation is linked to the stable sessions row. Replacing
|
||||||
|
* child exercise/set rows must not delete or detach it.
|
||||||
|
*/
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_list_body_metric_points(
|
||||||
|
database,
|
||||||
|
TRAINLOG_BODY_METRIC_WEIGHT,
|
||||||
|
weight_points,
|
||||||
|
4U,
|
||||||
|
&weight_count
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(weight_count == 1U);
|
||||||
|
CHECK(weight_points[0].value > 84.09);
|
||||||
|
CHECK(weight_points[0].value < 84.11);
|
||||||
|
|
||||||
|
bind_reps_exercise(
|
||||||
|
&bad[0],
|
||||||
|
rollback_sets,
|
||||||
|
1U,
|
||||||
|
95.0
|
||||||
|
);
|
||||||
|
|
||||||
|
bad[1] = bad[0];
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
bad[1].exercise_id,
|
||||||
|
sizeof(bad[1].exercise_id),
|
||||||
|
"%s",
|
||||||
|
"ex_missing"
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_replace_session_exercises(
|
||||||
|
database,
|
||||||
|
"se_edit",
|
||||||
|
bad,
|
||||||
|
2U
|
||||||
|
) == TRAINLOG_STATUS_NOT_FOUND
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The failed replacement deleted and reinserted rows inside one
|
||||||
|
* transaction. Rollback must restore the previous 85 kg / 12,11 data.
|
||||||
|
*/
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_load_session_editable(
|
||||||
|
database,
|
||||||
|
"se_edit",
|
||||||
|
&loaded_session,
|
||||||
|
loaded_exercises,
|
||||||
|
4U,
|
||||||
|
&exercise_count,
|
||||||
|
loaded_sets,
|
||||||
|
16U,
|
||||||
|
&set_count
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(exercise_count == 1U);
|
||||||
|
CHECK(set_count == 2U);
|
||||||
|
CHECK(loaded_sets[0].reps == 12);
|
||||||
|
CHECK(loaded_sets[1].reps == 11);
|
||||||
|
CHECK(loaded_sets[0].weight_kg > 84.99);
|
||||||
|
CHECK(loaded_sets[0].weight_kg < 85.01);
|
||||||
|
|
||||||
|
trainlog_database_close(database);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
CHECK(test_load_replace_and_rollback());
|
||||||
|
|
||||||
|
(void)printf(
|
||||||
|
"PASS session_edit\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue