Add profile-aware continuous exercise tracking
This commit is contained in:
parent
a3eeb91869
commit
db349c609d
17 changed files with 2677 additions and 280 deletions
|
|
@ -260,3 +260,27 @@ Transport/export is added only after the local recorder workflow is comfortable.
|
||||||
The Android client must preserve stable exercise IDs so a catalog snapshot from
|
The Android client must preserve stable exercise IDs so a catalog snapshot from
|
||||||
the PC can normalize exercise selection on both sides.
|
the PC can normalize exercise selection on both sides.
|
||||||
<!-- TRAINLOG_ANDROID_NEXT_SLICE _END -->
|
<!-- TRAINLOG_ANDROID_NEXT_SLICE _END -->
|
||||||
|
|
||||||
|
<!-- TRAINLOG_ANDROID_PROFILE_AWARE_ENTRY -->
|
||||||
|
## Profile-aware Android forms
|
||||||
|
|
||||||
|
Android uses the same exercise metadata as the TUI:
|
||||||
|
|
||||||
|
```text
|
||||||
|
recording_mode
|
||||||
|
tracking_mode
|
||||||
|
data_fields
|
||||||
|
```
|
||||||
|
|
||||||
|
A continuous `Marche` configured with speed displays only:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Durée
|
||||||
|
Vitesse
|
||||||
|
```
|
||||||
|
|
||||||
|
and no set count.
|
||||||
|
|
||||||
|
Creating an exercise directly inside session entry must configure this metadata
|
||||||
|
before adding it to the catalog/session.
|
||||||
|
<!-- TRAINLOG_ANDROID_PROFILE_AWARE_ENTRY _END -->
|
||||||
|
|
|
||||||
|
|
@ -282,3 +282,25 @@ python tools/validate_json.py
|
||||||
python tools/validate_import_contract.py
|
python tools/validate_import_contract.py
|
||||||
git diff --check
|
git diff --check
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<!-- TRAINLOG_EXERCISE_DATA_MODEL_V1 -->
|
||||||
|
## Exercise recording metadata — schema v3 direction
|
||||||
|
|
||||||
|
The next SQLite migration adds:
|
||||||
|
|
||||||
|
```text
|
||||||
|
recording_mode = sets | continuous
|
||||||
|
data_fields = bounded bit mask
|
||||||
|
```
|
||||||
|
|
||||||
|
Existing `tracking_mode = reps | duration` remains stable.
|
||||||
|
|
||||||
|
Every `session_exercises` row snapshots this metadata so later catalog changes
|
||||||
|
do not reinterpret historical sessions.
|
||||||
|
|
||||||
|
Migration v2 -> v3 defaults all existing rows to `sets` with `data_fields = 0`.
|
||||||
|
No name-based migration is allowed.
|
||||||
|
|
||||||
|
Continuous actual activity data is stored separately from `performed_sets`;
|
||||||
|
Trainlog will not manufacture a fake one-set workout.
|
||||||
|
<!-- TRAINLOG_EXERCISE_DATA_MODEL_V1 _END -->
|
||||||
|
|
|
||||||
195
docs/exercise_data_model.md
Normal file
195
docs/exercise_data_model.md
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
# Exercise data model
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
```text
|
||||||
|
EXERCISE_DATA_MODEL_V1=FROZEN_FOR_IMPLEMENTATION
|
||||||
|
DATABASE_SCHEMA_V3=NEXT
|
||||||
|
TUI_PROFILE_AWARE_ENTRY=AFTER_SCHEMA_V3
|
||||||
|
ANDROID_PROFILE_AWARE_ENTRY=AFTER_TUI
|
||||||
|
TRAINLOG_FORMAT_V1=FROZEN
|
||||||
|
SESSION_EXCHANGE_V2=DESIGN_REQUIRED_LATER
|
||||||
|
```
|
||||||
|
|
||||||
|
Trainlog must not use one universal exercise form.
|
||||||
|
|
||||||
|
An exercise is defined by three independent pieces of metadata:
|
||||||
|
|
||||||
|
```text
|
||||||
|
recording_mode = SETS | CONTINUOUS
|
||||||
|
tracking_mode = REPS | DURATION
|
||||||
|
data_fields = supplemental field bit mask
|
||||||
|
```
|
||||||
|
|
||||||
|
Initial valid combinations:
|
||||||
|
|
||||||
|
```text
|
||||||
|
SETS + REPS
|
||||||
|
SETS + DURATION
|
||||||
|
CONTINUOUS + DURATION
|
||||||
|
```
|
||||||
|
|
||||||
|
`CONTINUOUS + REPS` is invalid in model v1.
|
||||||
|
|
||||||
|
Initial supplemental fields:
|
||||||
|
|
||||||
|
```text
|
||||||
|
SPEED_KMH
|
||||||
|
DISTANCE_KM
|
||||||
|
```
|
||||||
|
|
||||||
|
Unknown field bits are invalid.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Presse à cuisses
|
||||||
|
SETS + REPS
|
||||||
|
|
||||||
|
Gainage
|
||||||
|
SETS + DURATION
|
||||||
|
|
||||||
|
Marche
|
||||||
|
CONTINUOUS + DURATION
|
||||||
|
SPEED_KMH
|
||||||
|
|
||||||
|
Course
|
||||||
|
CONTINUOUS + DURATION
|
||||||
|
SPEED_KMH
|
||||||
|
|
||||||
|
Vélo
|
||||||
|
CONTINUOUS + DURATION
|
||||||
|
SPEED_KMH | DISTANCE_KM
|
||||||
|
|
||||||
|
Rameur
|
||||||
|
CONTINUOUS + DURATION
|
||||||
|
DISTANCE_KM
|
||||||
|
```
|
||||||
|
|
||||||
|
Load semantics remain separate and session-specific:
|
||||||
|
|
||||||
|
```text
|
||||||
|
none
|
||||||
|
external
|
||||||
|
assistance
|
||||||
|
```
|
||||||
|
|
||||||
|
A continuous exercise does not ask for:
|
||||||
|
|
||||||
|
```text
|
||||||
|
number of sets
|
||||||
|
repetitions
|
||||||
|
per-set rest
|
||||||
|
```
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Marche
|
||||||
|
|
||||||
|
Durée 45 min
|
||||||
|
Vitesse 5.8 km/h
|
||||||
|
```
|
||||||
|
|
||||||
|
Creating/editing an exercise asks for:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Name
|
||||||
|
Organization: Sets | Continuous
|
||||||
|
Primary metric: Repetitions | Duration
|
||||||
|
Supplemental fields: Speed | Distance
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- continuous forces duration in model v1;
|
||||||
|
- sets accepts reps or duration;
|
||||||
|
- UI fields are driven by metadata, never exercise-name heuristics;
|
||||||
|
- changing catalog metadata affects future entry only.
|
||||||
|
|
||||||
|
Historical stability:
|
||||||
|
|
||||||
|
Every session exercise stores a snapshot of:
|
||||||
|
|
||||||
|
```text
|
||||||
|
recording_mode
|
||||||
|
tracking_mode
|
||||||
|
data_fields
|
||||||
|
```
|
||||||
|
|
||||||
|
So changing `Marche` from an old set-based duration exercise to continuous
|
||||||
|
duration + speed does not reinterpret old sessions.
|
||||||
|
|
||||||
|
## SQLite schema v3 direction
|
||||||
|
|
||||||
|
Schema v3 adds to `exercises`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
recording_mode
|
||||||
|
data_fields
|
||||||
|
```
|
||||||
|
|
||||||
|
and snapshots the same values in `session_exercises`.
|
||||||
|
|
||||||
|
Migration v2 -> v3 is conservative:
|
||||||
|
|
||||||
|
```text
|
||||||
|
all existing exercises -> SETS
|
||||||
|
all existing session exercises -> SETS
|
||||||
|
data_fields -> 0
|
||||||
|
```
|
||||||
|
|
||||||
|
No migration guesses by exercise name.
|
||||||
|
|
||||||
|
Continuous actual activity data gets its own one-to-one record:
|
||||||
|
|
||||||
|
```text
|
||||||
|
duration_seconds
|
||||||
|
speed_kmh nullable
|
||||||
|
distance_km nullable
|
||||||
|
```
|
||||||
|
|
||||||
|
Continuous activities have no `performed_sets` rows and no fake one-set
|
||||||
|
representation.
|
||||||
|
|
||||||
|
## Frozen JSON v1
|
||||||
|
|
||||||
|
Trainlog JSON session v1 remains frozen.
|
||||||
|
|
||||||
|
It represents the existing set-based exchange model.
|
||||||
|
|
||||||
|
Continuous data that cannot be represented in v1 must not be:
|
||||||
|
|
||||||
|
- hidden in notes;
|
||||||
|
- converted into a fake set;
|
||||||
|
- silently discarded.
|
||||||
|
|
||||||
|
A future explicit session exchange v2 will carry profile-aware exercise data
|
||||||
|
while v1 import remains supported.
|
||||||
|
|
||||||
|
## Android/TUI parity
|
||||||
|
|
||||||
|
Both interfaces consume identical exercise metadata.
|
||||||
|
|
||||||
|
The future PC -> Android catalog snapshot must contain:
|
||||||
|
|
||||||
|
```text
|
||||||
|
exercise_id
|
||||||
|
name
|
||||||
|
recording_mode
|
||||||
|
tracking_mode
|
||||||
|
data_fields
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation order
|
||||||
|
|
||||||
|
```text
|
||||||
|
1. SQLite schema v3 + migration tests
|
||||||
|
2. C model/API additions
|
||||||
|
3. catalog create/edit support
|
||||||
|
4. TUI profile-aware session entry
|
||||||
|
5. manually convert Marche to CONTINUOUS + SPEED_KMH
|
||||||
|
6. profile-aware detail/history
|
||||||
|
7. Android uses the same model
|
||||||
|
8. design session exchange v2
|
||||||
|
```
|
||||||
|
|
@ -285,3 +285,27 @@ Fictitious sessions, exercises, and body observations are used during Android
|
||||||
development and synchronization testing. The development database will be
|
development and synchronization testing. The development database will be
|
||||||
purged before normal production use begins.
|
purged before normal production use begins.
|
||||||
<!-- TRAINLOG_SYNC_FINAL_ROADMAP _END -->
|
<!-- TRAINLOG_SYNC_FINAL_ROADMAP _END -->
|
||||||
|
|
||||||
|
<!-- TRAINLOG_EXERCISE_DATA_MODEL_ROADMAP -->
|
||||||
|
## Exercise data-model checkpoint
|
||||||
|
|
||||||
|
```text
|
||||||
|
EXERCISE_DATA_MODEL_V1=FROZEN_FOR_IMPLEMENTATION
|
||||||
|
DATABASE_SCHEMA_V3=NEXT
|
||||||
|
PROFILE_AWARE_C_MODEL=AFTER_SCHEMA
|
||||||
|
PROFILE_AWARE_TUI=AFTER
|
||||||
|
ANDROID_PROFILE_AWARE_UI=AFTER
|
||||||
|
SESSION_EXCHANGE_V2=DESIGN_LATER
|
||||||
|
TRAINLOG_FORMAT_V1=FROZEN
|
||||||
|
```
|
||||||
|
|
||||||
|
The model separates:
|
||||||
|
|
||||||
|
```text
|
||||||
|
recording organization: SETS | CONTINUOUS
|
||||||
|
primary metric: REPS | DURATION
|
||||||
|
supplemental fields: SPEED_KMH | DISTANCE_KM
|
||||||
|
```
|
||||||
|
|
||||||
|
Existing schema-v2 data migrates conservatively to `SETS`.
|
||||||
|
<!-- TRAINLOG_EXERCISE_DATA_MODEL_ROADMAP _END -->
|
||||||
|
|
|
||||||
22
docs/tui.md
22
docs/tui.md
|
|
@ -674,3 +674,25 @@ PC -> Android
|
||||||
The session exchange remains frozen Trainlog JSON v1.
|
The session exchange remains frozen Trainlog JSON v1.
|
||||||
Catalog synchronization is a separate versioned contract.
|
Catalog synchronization is a separate versioned contract.
|
||||||
<!-- TRAINLOG_SYNC_FINAL_CHECKPOINT _END -->
|
<!-- TRAINLOG_SYNC_FINAL_CHECKPOINT _END -->
|
||||||
|
|
||||||
|
<!-- TRAINLOG_PROFILE_AWARE_ENTRY -->
|
||||||
|
## Profile-aware exercise entry
|
||||||
|
|
||||||
|
The TUI form is driven by exercise metadata.
|
||||||
|
|
||||||
|
```text
|
||||||
|
SETS + REPS
|
||||||
|
series, reps, optional load, rest
|
||||||
|
|
||||||
|
SETS + DURATION
|
||||||
|
series, duration, optional load, rest
|
||||||
|
|
||||||
|
CONTINUOUS + DURATION + SPEED_KMH
|
||||||
|
duration, speed
|
||||||
|
```
|
||||||
|
|
||||||
|
Continuous exercises do not display a set count.
|
||||||
|
|
||||||
|
`Marche` will use the continuous form only after its catalog metadata is
|
||||||
|
explicitly changed; behavior is never inferred from its name.
|
||||||
|
<!-- TRAINLOG_PROFILE_AWARE_ENTRY _END -->
|
||||||
|
|
|
||||||
|
|
@ -40,4 +40,13 @@ TrainlogStatus trainlog_catalog_create_exercise(
|
||||||
TrainlogExercise *output_exercise
|
TrainlogExercise *output_exercise
|
||||||
);
|
);
|
||||||
|
|
||||||
|
TrainlogStatus trainlog_catalog_create_exercise_profiled(
|
||||||
|
TrainlogDatabase *database,
|
||||||
|
const char *name,
|
||||||
|
TrainlogTrackingMode tracking_mode,
|
||||||
|
TrainlogRecordingMode recording_mode,
|
||||||
|
TrainlogExerciseDataFields data_fields,
|
||||||
|
TrainlogExercise *output_exercise
|
||||||
|
);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
#include "trainlog/model.h"
|
#include "trainlog/model.h"
|
||||||
#include "trainlog/status.h"
|
#include "trainlog/status.h"
|
||||||
|
|
||||||
#define TRAINLOG_DATABASE_SCHEMA_VERSION 2
|
#define TRAINLOG_DATABASE_SCHEMA_VERSION 4
|
||||||
|
|
||||||
typedef struct TrainlogDatabase TrainlogDatabase;
|
typedef struct TrainlogDatabase TrainlogDatabase;
|
||||||
|
|
||||||
|
|
@ -50,6 +50,16 @@ TrainlogStatus trainlog_database_insert_exercise(
|
||||||
TrainlogTrackingMode tracking_mode
|
TrainlogTrackingMode tracking_mode
|
||||||
);
|
);
|
||||||
|
|
||||||
|
TrainlogStatus trainlog_database_insert_exercise_profiled(
|
||||||
|
TrainlogDatabase *database,
|
||||||
|
const char *exercise_id,
|
||||||
|
const char *name,
|
||||||
|
const char *normalized_name,
|
||||||
|
TrainlogTrackingMode tracking_mode,
|
||||||
|
TrainlogRecordingMode recording_mode,
|
||||||
|
TrainlogExerciseDataFields data_fields
|
||||||
|
);
|
||||||
|
|
||||||
TrainlogStatus trainlog_database_exercise_count(
|
TrainlogStatus trainlog_database_exercise_count(
|
||||||
TrainlogDatabase *database,
|
TrainlogDatabase *database,
|
||||||
size_t *output_count
|
size_t *output_count
|
||||||
|
|
@ -99,6 +109,8 @@ TrainlogStatus trainlog_database_list_weight_points(
|
||||||
typedef struct TrainlogPersistedExerciseDetail {
|
typedef struct TrainlogPersistedExerciseDetail {
|
||||||
char name[TRAINLOG_NAME_MAX + 1U];
|
char name[TRAINLOG_NAME_MAX + 1U];
|
||||||
TrainlogTrackingMode tracking_mode;
|
TrainlogTrackingMode tracking_mode;
|
||||||
|
TrainlogRecordingMode recording_mode;
|
||||||
|
TrainlogExerciseDataFields data_fields;
|
||||||
TrainlogLoadMode load_mode;
|
TrainlogLoadMode load_mode;
|
||||||
int rest_seconds;
|
int rest_seconds;
|
||||||
int target_sets;
|
int target_sets;
|
||||||
|
|
@ -106,6 +118,13 @@ typedef struct TrainlogPersistedExerciseDetail {
|
||||||
int target_duration_seconds;
|
int target_duration_seconds;
|
||||||
int has_target_weight;
|
int has_target_weight;
|
||||||
double target_weight_kg;
|
double target_weight_kg;
|
||||||
|
|
||||||
|
int continuous_duration_seconds;
|
||||||
|
int has_continuous_speed;
|
||||||
|
double continuous_speed_kmh;
|
||||||
|
int has_continuous_distance;
|
||||||
|
double continuous_distance_km;
|
||||||
|
|
||||||
size_t actual_set_count;
|
size_t actual_set_count;
|
||||||
char actual_summary[TRAINLOG_SET_SUMMARY_MAX + 1U];
|
char actual_summary[TRAINLOG_SET_SUMMARY_MAX + 1U];
|
||||||
} TrainlogPersistedExerciseDetail;
|
} TrainlogPersistedExerciseDetail;
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
#define TRAINLOG_ID_MAX 128U
|
#define TRAINLOG_ID_MAX 128U
|
||||||
#define TRAINLOG_NAME_MAX 200U
|
#define TRAINLOG_NAME_MAX 200U
|
||||||
|
|
@ -19,6 +20,19 @@ typedef enum TrainlogTrackingMode {
|
||||||
TRAINLOG_TRACKING_DURATION
|
TRAINLOG_TRACKING_DURATION
|
||||||
} TrainlogTrackingMode;
|
} TrainlogTrackingMode;
|
||||||
|
|
||||||
|
typedef enum TrainlogRecordingMode {
|
||||||
|
TRAINLOG_RECORDING_SETS = 0,
|
||||||
|
TRAINLOG_RECORDING_CONTINUOUS
|
||||||
|
} TrainlogRecordingMode;
|
||||||
|
|
||||||
|
typedef uint32_t TrainlogExerciseDataFields;
|
||||||
|
|
||||||
|
#define TRAINLOG_EXERCISE_DATA_SPEED_KMH UINT32_C(1)
|
||||||
|
#define TRAINLOG_EXERCISE_DATA_DISTANCE_KM UINT32_C(2)
|
||||||
|
#define TRAINLOG_EXERCISE_DATA_KNOWN_MASK \
|
||||||
|
(TRAINLOG_EXERCISE_DATA_SPEED_KMH | \
|
||||||
|
TRAINLOG_EXERCISE_DATA_DISTANCE_KM)
|
||||||
|
|
||||||
typedef enum TrainlogLoadMode {
|
typedef enum TrainlogLoadMode {
|
||||||
TRAINLOG_LOAD_NONE = 0,
|
TRAINLOG_LOAD_NONE = 0,
|
||||||
TRAINLOG_LOAD_EXTERNAL,
|
TRAINLOG_LOAD_EXTERNAL,
|
||||||
|
|
@ -34,6 +48,8 @@ typedef struct TrainlogExercise {
|
||||||
char exercise_id[TRAINLOG_ID_MAX + 1U];
|
char exercise_id[TRAINLOG_ID_MAX + 1U];
|
||||||
char name[TRAINLOG_NAME_MAX + 1U];
|
char name[TRAINLOG_NAME_MAX + 1U];
|
||||||
TrainlogTrackingMode tracking_mode;
|
TrainlogTrackingMode tracking_mode;
|
||||||
|
TrainlogRecordingMode recording_mode;
|
||||||
|
TrainlogExerciseDataFields data_fields;
|
||||||
} TrainlogExercise;
|
} TrainlogExercise;
|
||||||
|
|
||||||
typedef struct TrainlogSetInput {
|
typedef struct TrainlogSetInput {
|
||||||
|
|
@ -45,6 +61,8 @@ typedef struct TrainlogSetInput {
|
||||||
|
|
||||||
typedef struct TrainlogSessionExerciseInput {
|
typedef struct TrainlogSessionExerciseInput {
|
||||||
char exercise_id[TRAINLOG_ID_MAX + 1U];
|
char exercise_id[TRAINLOG_ID_MAX + 1U];
|
||||||
|
TrainlogRecordingMode recording_mode;
|
||||||
|
TrainlogExerciseDataFields data_fields;
|
||||||
TrainlogLoadMode load_mode;
|
TrainlogLoadMode load_mode;
|
||||||
int rest_seconds;
|
int rest_seconds;
|
||||||
int target_sets;
|
int target_sets;
|
||||||
|
|
@ -52,6 +70,13 @@ typedef struct TrainlogSessionExerciseInput {
|
||||||
int target_duration_seconds;
|
int target_duration_seconds;
|
||||||
bool target_has_weight;
|
bool target_has_weight;
|
||||||
double target_weight_kg;
|
double target_weight_kg;
|
||||||
|
|
||||||
|
int continuous_duration_seconds;
|
||||||
|
bool continuous_has_speed;
|
||||||
|
double continuous_speed_kmh;
|
||||||
|
bool continuous_has_distance;
|
||||||
|
double continuous_distance_km;
|
||||||
|
|
||||||
const char *notes;
|
const char *notes;
|
||||||
const TrainlogSetInput *sets;
|
const TrainlogSetInput *sets;
|
||||||
size_t set_count;
|
size_t set_count;
|
||||||
|
|
|
||||||
|
|
@ -243,3 +243,39 @@ trainlog_mtp_roundtrip_probe = executable(
|
||||||
dependencies: trainlog_core_dep,
|
dependencies: trainlog_core_dep,
|
||||||
c_args: strict_c_args,
|
c_args: strict_c_args,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
test_exercise_profile_schema = executable(
|
||||||
|
'test_exercise_profile_schema',
|
||||||
|
'tests/test_exercise_profile_schema.c',
|
||||||
|
dependencies: trainlog_core_dep,
|
||||||
|
c_args: strict_c_args,
|
||||||
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
'exercise_profile_schema',
|
||||||
|
test_exercise_profile_schema,
|
||||||
|
)
|
||||||
|
|
||||||
|
test_continuous_session = executable(
|
||||||
|
'test_continuous_session',
|
||||||
|
'tests/test_continuous_session.c',
|
||||||
|
dependencies: trainlog_core_dep,
|
||||||
|
c_args: strict_c_args,
|
||||||
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
'continuous_session',
|
||||||
|
test_continuous_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
test_continuous_detail = executable(
|
||||||
|
'test_continuous_detail',
|
||||||
|
'tests/test_continuous_detail.c',
|
||||||
|
dependencies: trainlog_core_dep,
|
||||||
|
c_args: strict_c_args,
|
||||||
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
'continuous_detail',
|
||||||
|
test_continuous_detail,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -131,10 +131,12 @@ TrainlogStatus trainlog_catalog_normalize_name(
|
||||||
return TRAINLOG_STATUS_OK;
|
return TRAINLOG_STATUS_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
TrainlogStatus trainlog_catalog_create_exercise(
|
TrainlogStatus trainlog_catalog_create_exercise_profiled(
|
||||||
TrainlogDatabase *database,
|
TrainlogDatabase *database,
|
||||||
const char *name,
|
const char *name,
|
||||||
TrainlogTrackingMode tracking_mode,
|
TrainlogTrackingMode tracking_mode,
|
||||||
|
TrainlogRecordingMode recording_mode,
|
||||||
|
TrainlogExerciseDataFields data_fields,
|
||||||
TrainlogExercise *output_exercise
|
TrainlogExercise *output_exercise
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
|
@ -154,6 +156,7 @@ TrainlogStatus trainlog_catalog_create_exercise(
|
||||||
normalized,
|
normalized,
|
||||||
sizeof(normalized)
|
sizeof(normalized)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (status != TRAINLOG_STATUS_OK) {
|
if (status != TRAINLOG_STATUS_OK) {
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
@ -163,35 +166,65 @@ TrainlogStatus trainlog_catalog_create_exercise(
|
||||||
exercise_id,
|
exercise_id,
|
||||||
sizeof(exercise_id)
|
sizeof(exercise_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (status != TRAINLOG_STATUS_OK) {
|
if (status != TRAINLOG_STATUS_OK) {
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
status = trainlog_database_insert_exercise(
|
status = trainlog_database_insert_exercise_profiled(
|
||||||
database,
|
database,
|
||||||
exercise_id,
|
exercise_id,
|
||||||
name,
|
name,
|
||||||
normalized,
|
normalized,
|
||||||
tracking_mode
|
tracking_mode,
|
||||||
|
recording_mode,
|
||||||
|
data_fields
|
||||||
);
|
);
|
||||||
|
|
||||||
if (status != TRAINLOG_STATUS_OK) {
|
if (status != TRAINLOG_STATUS_OK) {
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
(void)memset(output_exercise, 0, sizeof(*output_exercise));
|
(void)memset(
|
||||||
|
output_exercise,
|
||||||
|
0,
|
||||||
|
sizeof(*output_exercise)
|
||||||
|
);
|
||||||
|
|
||||||
(void)snprintf(
|
(void)snprintf(
|
||||||
output_exercise->exercise_id,
|
output_exercise->exercise_id,
|
||||||
sizeof(output_exercise->exercise_id),
|
sizeof(output_exercise->exercise_id),
|
||||||
"%s",
|
"%s",
|
||||||
exercise_id
|
exercise_id
|
||||||
);
|
);
|
||||||
|
|
||||||
(void)snprintf(
|
(void)snprintf(
|
||||||
output_exercise->name,
|
output_exercise->name,
|
||||||
sizeof(output_exercise->name),
|
sizeof(output_exercise->name),
|
||||||
"%s",
|
"%s",
|
||||||
name
|
name
|
||||||
);
|
);
|
||||||
|
|
||||||
output_exercise->tracking_mode = tracking_mode;
|
output_exercise->tracking_mode = tracking_mode;
|
||||||
|
output_exercise->recording_mode = recording_mode;
|
||||||
|
output_exercise->data_fields = data_fields;
|
||||||
|
|
||||||
return TRAINLOG_STATUS_OK;
|
return TRAINLOG_STATUS_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TrainlogStatus trainlog_catalog_create_exercise(
|
||||||
|
TrainlogDatabase *database,
|
||||||
|
const char *name,
|
||||||
|
TrainlogTrackingMode tracking_mode,
|
||||||
|
TrainlogExercise *output_exercise
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return trainlog_catalog_create_exercise_profiled(
|
||||||
|
database,
|
||||||
|
name,
|
||||||
|
tracking_mode,
|
||||||
|
TRAINLOG_RECORDING_SETS,
|
||||||
|
0U,
|
||||||
|
output_exercise
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
711
tui/src/tui.c
711
tui/src/tui.c
|
|
@ -38,6 +38,7 @@
|
||||||
|
|
||||||
|
|
||||||
/* TRAINLOG_TUI_V02_POLISH */
|
/* TRAINLOG_TUI_V02_POLISH */
|
||||||
|
/* TRAINLOG_TUI_PROFILED_EXERCISE_CREATION */
|
||||||
|
|
||||||
typedef enum DashboardAction {
|
typedef enum DashboardAction {
|
||||||
DASHBOARD_NEW_SESSION = 0,
|
DASHBOARD_NEW_SESSION = 0,
|
||||||
|
|
@ -1756,7 +1757,11 @@ if (primary_top_nav_activate(
|
||||||
if (key == 'a' ||
|
if (key == 'a' ||
|
||||||
key == 'A') {
|
key == 'A') {
|
||||||
char name[TRAINLOG_NAME_MAX + 1U];
|
char name[TRAINLOG_NAME_MAX + 1U];
|
||||||
int mode = 1;
|
int mode = 1;
|
||||||
|
int organization = 1;
|
||||||
|
int wants_speed = 0;
|
||||||
|
int wants_distance = 0;
|
||||||
|
TrainlogExerciseDataFields data_fields = 0U;
|
||||||
TrainlogExercise created;
|
TrainlogExercise created;
|
||||||
TrainlogStatus status;
|
TrainlogStatus status;
|
||||||
|
|
||||||
|
|
@ -1786,14 +1791,64 @@ if (primary_top_nav_activate(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
status =
|
if (!prompt_int_value(
|
||||||
trainlog_catalog_create_exercise(
|
8,
|
||||||
|
"Organisation (1 séries, 2 continu)",
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
&organization
|
||||||
|
)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organization == 2) {
|
||||||
|
mode = 2;
|
||||||
|
|
||||||
|
if (!prompt_int_value(
|
||||||
|
10,
|
||||||
|
"Mesurer la vitesse km/h ? (0 non, 1 oui)",
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
&wants_speed
|
||||||
|
)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!prompt_int_value(
|
||||||
|
12,
|
||||||
|
"Mesurer la distance km ? (0 non, 1 oui)",
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
&wants_distance
|
||||||
|
)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wants_speed != 0) {
|
||||||
|
data_fields |=
|
||||||
|
TRAINLOG_EXERCISE_DATA_SPEED_KMH;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wants_distance != 0) {
|
||||||
|
data_fields |=
|
||||||
|
TRAINLOG_EXERCISE_DATA_DISTANCE_KM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
status =
|
||||||
|
trainlog_catalog_create_exercise_profiled(
|
||||||
database,
|
database,
|
||||||
name,
|
name,
|
||||||
mode == 1
|
mode == 1
|
||||||
? TRAINLOG_TRACKING_REPS
|
? TRAINLOG_TRACKING_REPS
|
||||||
: TRAINLOG_TRACKING_DURATION,
|
: TRAINLOG_TRACKING_DURATION,
|
||||||
&created
|
organization == 2
|
||||||
|
? TRAINLOG_RECORDING_CONTINUOUS
|
||||||
|
: TRAINLOG_RECORDING_SETS,
|
||||||
|
data_fields,
|
||||||
|
&created
|
||||||
);
|
);
|
||||||
|
|
||||||
if (status ==
|
if (status ==
|
||||||
|
|
@ -1827,7 +1882,11 @@ static bool create_exercise_inline(
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
char name[TRAINLOG_NAME_MAX + 1U];
|
char name[TRAINLOG_NAME_MAX + 1U];
|
||||||
int mode = 1;
|
int mode = 1;
|
||||||
|
int organization = 1;
|
||||||
|
int wants_speed = 0;
|
||||||
|
int wants_distance = 0;
|
||||||
|
TrainlogExerciseDataFields data_fields = 0U;
|
||||||
TrainlogExercise created;
|
TrainlogExercise created;
|
||||||
TrainlogStatus status;
|
TrainlogStatus status;
|
||||||
|
|
||||||
|
|
@ -1857,14 +1916,64 @@ static bool create_exercise_inline(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
status =
|
if (!prompt_int_value(
|
||||||
trainlog_catalog_create_exercise(
|
8,
|
||||||
|
"Organisation (1 séries, 2 continu)",
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
&organization
|
||||||
|
)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organization == 2) {
|
||||||
|
mode = 2;
|
||||||
|
|
||||||
|
if (!prompt_int_value(
|
||||||
|
10,
|
||||||
|
"Mesurer la vitesse km/h ? (0 non, 1 oui)",
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
&wants_speed
|
||||||
|
)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!prompt_int_value(
|
||||||
|
12,
|
||||||
|
"Mesurer la distance km ? (0 non, 1 oui)",
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
&wants_distance
|
||||||
|
)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wants_speed != 0) {
|
||||||
|
data_fields |=
|
||||||
|
TRAINLOG_EXERCISE_DATA_SPEED_KMH;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wants_distance != 0) {
|
||||||
|
data_fields |=
|
||||||
|
TRAINLOG_EXERCISE_DATA_DISTANCE_KM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
status =
|
||||||
|
trainlog_catalog_create_exercise_profiled(
|
||||||
database,
|
database,
|
||||||
name,
|
name,
|
||||||
mode == 1
|
mode == 1
|
||||||
? TRAINLOG_TRACKING_REPS
|
? TRAINLOG_TRACKING_REPS
|
||||||
: TRAINLOG_TRACKING_DURATION,
|
: TRAINLOG_TRACKING_DURATION,
|
||||||
&created
|
organization == 2
|
||||||
|
? TRAINLOG_RECORDING_CONTINUOUS
|
||||||
|
: TRAINLOG_RECORDING_SETS,
|
||||||
|
data_fields,
|
||||||
|
&created
|
||||||
);
|
);
|
||||||
|
|
||||||
if (status == TRAINLOG_STATUS_OK) {
|
if (status == TRAINLOG_STATUS_OK) {
|
||||||
|
|
@ -4378,12 +4487,119 @@ static bool build_session_exercise(
|
||||||
double target_weight = 0.0;
|
double target_weight = 0.0;
|
||||||
size_t set_index;
|
size_t set_index;
|
||||||
|
|
||||||
if (!choose_exercise(database, &exercise)) {
|
if (!choose_exercise(
|
||||||
|
database,
|
||||||
|
&exercise
|
||||||
|
)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(void)memset(
|
||||||
|
output,
|
||||||
|
0,
|
||||||
|
sizeof(*output)
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
output->exercise_id,
|
||||||
|
sizeof(output->exercise_id),
|
||||||
|
"%s",
|
||||||
|
exercise.exercise_id
|
||||||
|
);
|
||||||
|
|
||||||
|
output->recording_mode =
|
||||||
|
exercise.recording_mode;
|
||||||
|
|
||||||
|
output->data_fields =
|
||||||
|
exercise.data_fields;
|
||||||
|
|
||||||
|
if (exercise.recording_mode ==
|
||||||
|
TRAINLOG_RECORDING_CONTINUOUS) {
|
||||||
|
int duration_minutes = 30;
|
||||||
|
int duration_seconds;
|
||||||
|
bool has_value = false;
|
||||||
|
double value = 0.0;
|
||||||
|
|
||||||
|
draw_shell(
|
||||||
|
exercise.name,
|
||||||
|
"Échap annuler · activité continue"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!prompt_int_value(
|
||||||
|
4,
|
||||||
|
"Durée (minutes)",
|
||||||
|
1,
|
||||||
|
1440,
|
||||||
|
duration_minutes,
|
||||||
|
&duration_minutes
|
||||||
|
)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
duration_seconds =
|
||||||
|
duration_minutes * 60;
|
||||||
|
|
||||||
|
output->continuous_duration_seconds =
|
||||||
|
duration_seconds;
|
||||||
|
|
||||||
|
output->load_mode =
|
||||||
|
TRAINLOG_LOAD_NONE;
|
||||||
|
|
||||||
|
output->rest_seconds = 0;
|
||||||
|
output->target_sets = 0;
|
||||||
|
output->target_reps = 0;
|
||||||
|
output->target_duration_seconds = 0;
|
||||||
|
output->target_has_weight = false;
|
||||||
|
output->sets = NULL;
|
||||||
|
output->set_count = 0U;
|
||||||
|
|
||||||
|
if ((exercise.data_fields &
|
||||||
|
TRAINLOG_EXERCISE_DATA_SPEED_KMH) != 0U) {
|
||||||
|
if (!prompt_optional_double(
|
||||||
|
5,
|
||||||
|
"Vitesse km/h : ",
|
||||||
|
&has_value,
|
||||||
|
&value
|
||||||
|
) ||
|
||||||
|
!has_value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
output->continuous_has_speed =
|
||||||
|
true;
|
||||||
|
|
||||||
|
output->continuous_speed_kmh =
|
||||||
|
value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((exercise.data_fields &
|
||||||
|
TRAINLOG_EXERCISE_DATA_DISTANCE_KM) != 0U) {
|
||||||
|
has_value = false;
|
||||||
|
value = 0.0;
|
||||||
|
|
||||||
|
if (!prompt_optional_double(
|
||||||
|
6,
|
||||||
|
"Distance km : ",
|
||||||
|
&has_value,
|
||||||
|
&value
|
||||||
|
) ||
|
||||||
|
!has_value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
output->continuous_has_distance =
|
||||||
|
true;
|
||||||
|
|
||||||
|
output->continuous_distance_km =
|
||||||
|
value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
target_metric =
|
target_metric =
|
||||||
exercise.tracking_mode == TRAINLOG_TRACKING_REPS
|
exercise.tracking_mode ==
|
||||||
|
TRAINLOG_TRACKING_REPS
|
||||||
? 10
|
? 10
|
||||||
: 45;
|
: 45;
|
||||||
|
|
||||||
|
|
@ -4409,7 +4625,8 @@ static bool build_session_exercise(
|
||||||
"Charge cible kg : ",
|
"Charge cible kg : ",
|
||||||
&target_has_weight,
|
&target_has_weight,
|
||||||
&target_weight
|
&target_weight
|
||||||
) || !target_has_weight) {
|
) ||
|
||||||
|
!target_has_weight) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4425,7 +4642,8 @@ static bool build_session_exercise(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exercise.tracking_mode == TRAINLOG_TRACKING_REPS) {
|
if (exercise.tracking_mode ==
|
||||||
|
TRAINLOG_TRACKING_REPS) {
|
||||||
if (!prompt_int_value(
|
if (!prompt_int_value(
|
||||||
7,
|
7,
|
||||||
"Répétitions cibles",
|
"Répétitions cibles",
|
||||||
|
|
@ -4471,42 +4689,45 @@ static bool build_session_exercise(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
(void)memset(output, 0, sizeof(*output));
|
|
||||||
|
|
||||||
(void)snprintf(
|
|
||||||
output->exercise_id,
|
|
||||||
sizeof(output->exercise_id),
|
|
||||||
"%s",
|
|
||||||
exercise.exercise_id
|
|
||||||
);
|
|
||||||
|
|
||||||
output->load_mode =
|
output->load_mode =
|
||||||
load_mode == 1
|
load_mode == 1
|
||||||
? TRAINLOG_LOAD_NONE
|
? TRAINLOG_LOAD_NONE
|
||||||
: (load_mode == 2
|
: (
|
||||||
? TRAINLOG_LOAD_EXTERNAL
|
load_mode == 2
|
||||||
: TRAINLOG_LOAD_ASSISTANCE);
|
? TRAINLOG_LOAD_EXTERNAL
|
||||||
|
: TRAINLOG_LOAD_ASSISTANCE
|
||||||
|
);
|
||||||
|
|
||||||
output->rest_seconds = rest_seconds;
|
output->rest_seconds = rest_seconds;
|
||||||
output->target_sets = target_sets;
|
output->target_sets = target_sets;
|
||||||
|
|
||||||
output->target_reps =
|
output->target_reps =
|
||||||
exercise.tracking_mode == TRAINLOG_TRACKING_REPS
|
exercise.tracking_mode ==
|
||||||
|
TRAINLOG_TRACKING_REPS
|
||||||
? target_metric
|
? target_metric
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
output->target_duration_seconds =
|
output->target_duration_seconds =
|
||||||
exercise.tracking_mode == TRAINLOG_TRACKING_DURATION
|
exercise.tracking_mode ==
|
||||||
|
TRAINLOG_TRACKING_DURATION
|
||||||
? target_metric
|
? target_metric
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
output->target_has_weight = target_has_weight;
|
output->target_has_weight =
|
||||||
output->target_weight_kg = target_weight;
|
target_has_weight;
|
||||||
output->sets = set_storage;
|
|
||||||
output->set_count = (size_t)actual_sets;
|
|
||||||
|
|
||||||
for (set_index = 0U; set_index < output->set_count; ++set_index) {
|
output->target_weight_kg =
|
||||||
int actual_metric = target_metric;
|
target_weight;
|
||||||
|
|
||||||
|
output->sets = set_storage;
|
||||||
|
output->set_count =
|
||||||
|
(size_t)actual_sets;
|
||||||
|
|
||||||
|
for (set_index = 0U;
|
||||||
|
set_index < output->set_count;
|
||||||
|
++set_index) {
|
||||||
|
int actual_metric =
|
||||||
|
target_metric;
|
||||||
|
|
||||||
(void)memset(
|
(void)memset(
|
||||||
&set_storage[set_index],
|
&set_storage[set_index],
|
||||||
|
|
@ -4527,7 +4748,8 @@ static bool build_session_exercise(
|
||||||
output->set_count
|
output->set_count
|
||||||
);
|
);
|
||||||
|
|
||||||
if (exercise.tracking_mode == TRAINLOG_TRACKING_REPS) {
|
if (exercise.tracking_mode ==
|
||||||
|
TRAINLOG_TRACKING_REPS) {
|
||||||
if (!prompt_int_value(
|
if (!prompt_int_value(
|
||||||
5,
|
5,
|
||||||
"Répétitions réalisées",
|
"Répétitions réalisées",
|
||||||
|
|
@ -4539,7 +4761,8 @@ static bool build_session_exercise(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
set_storage[set_index].reps = actual_metric;
|
set_storage[set_index].reps =
|
||||||
|
actual_metric;
|
||||||
} else {
|
} else {
|
||||||
if (!prompt_duration_value(
|
if (!prompt_duration_value(
|
||||||
5,
|
5,
|
||||||
|
|
@ -4552,13 +4775,17 @@ static bool build_session_exercise(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
set_storage[set_index].duration_seconds = actual_metric;
|
set_storage[
|
||||||
|
set_index
|
||||||
|
].duration_seconds =
|
||||||
|
actual_metric;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (target_has_weight) {
|
if (target_has_weight) {
|
||||||
char buffer[64];
|
char buffer[64];
|
||||||
char prompt[128];
|
char prompt[128];
|
||||||
double actual_weight = target_weight;
|
double actual_weight =
|
||||||
|
target_weight;
|
||||||
|
|
||||||
(void)snprintf(
|
(void)snprintf(
|
||||||
prompt,
|
prompt,
|
||||||
|
|
@ -4586,12 +4813,20 @@ static bool build_session_exercise(
|
||||||
"Charge invalide.",
|
"Charge invalide.",
|
||||||
TRAINLOG_COLOR_ERROR
|
TRAINLOG_COLOR_ERROR
|
||||||
);
|
);
|
||||||
|
|
||||||
wait_key();
|
wait_key();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
set_storage[set_index].has_weight = true;
|
set_storage[
|
||||||
set_storage[set_index].weight_kg = actual_weight;
|
set_index
|
||||||
|
].has_weight =
|
||||||
|
true;
|
||||||
|
|
||||||
|
set_storage[
|
||||||
|
set_index
|
||||||
|
].weight_kg =
|
||||||
|
actual_weight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -6178,173 +6413,277 @@ static void screen_session_detail(
|
||||||
TrainlogPersistedExerciseDetail *exercise =
|
TrainlogPersistedExerciseDetail *exercise =
|
||||||
&exercises[selected];
|
&exercises[selected];
|
||||||
|
|
||||||
char rest_text[64];
|
if (exercise->recording_mode ==
|
||||||
char target_duration_text[64];
|
TRAINLOG_RECORDING_CONTINUOUS) {
|
||||||
|
char duration_text[64];
|
||||||
|
|
||||||
int title_row =
|
int title_row =
|
||||||
decorated ? 16 : 6;
|
decorated ? 16 : 6;
|
||||||
|
|
||||||
int mode_row =
|
int mode_row =
|
||||||
decorated ? 18 : 8;
|
decorated ? 18 : 8;
|
||||||
|
|
||||||
int target_row =
|
int first_data_row =
|
||||||
decorated ? 20 : 10;
|
decorated ? 20 : 10;
|
||||||
|
|
||||||
int weight_row =
|
int row =
|
||||||
decorated ? 21 : 11;
|
first_data_row;
|
||||||
|
|
||||||
int actual_row =
|
if (trainlog_duration_format(
|
||||||
decorated ? 23 : 13;
|
exercise->continuous_duration_seconds,
|
||||||
|
duration_text,
|
||||||
|
sizeof(duration_text)
|
||||||
|
) != TRAINLOG_STATUS_OK) {
|
||||||
|
(void)snprintf(
|
||||||
|
duration_text,
|
||||||
|
sizeof(duration_text),
|
||||||
|
"%d s",
|
||||||
|
exercise->continuous_duration_seconds
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
int summary_row =
|
|
||||||
decorated ? 25 : 15;
|
|
||||||
|
|
||||||
int warning_row =
|
|
||||||
decorated ? 27 : 17;
|
|
||||||
|
|
||||||
if (trainlog_duration_format(
|
|
||||||
exercise->rest_seconds,
|
|
||||||
rest_text,
|
|
||||||
sizeof(rest_text)
|
|
||||||
) != TRAINLOG_STATUS_OK) {
|
|
||||||
(void)snprintf(
|
|
||||||
rest_text,
|
|
||||||
sizeof(rest_text),
|
|
||||||
"%ds",
|
|
||||||
exercise->rest_seconds
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
target_duration_text[0] = '\0';
|
|
||||||
|
|
||||||
if (exercise->tracking_mode ==
|
|
||||||
TRAINLOG_TRACKING_DURATION) {
|
|
||||||
(void)trainlog_duration_format(
|
|
||||||
exercise->target_duration_seconds,
|
|
||||||
target_duration_text,
|
|
||||||
sizeof(target_duration_text)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
attron(
|
|
||||||
A_BOLD |
|
|
||||||
trainlog_theme_attribute(
|
|
||||||
TRAINLOG_COLOR_ACCENT
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
mvprintw(
|
|
||||||
title_row,
|
|
||||||
decorated ? 5 : 4,
|
|
||||||
"Exercice %zu/%zu — %s",
|
|
||||||
selected + 1U,
|
|
||||||
count,
|
|
||||||
exercise->name
|
|
||||||
);
|
|
||||||
|
|
||||||
attroff(
|
|
||||||
A_BOLD |
|
|
||||||
trainlog_theme_attribute(
|
|
||||||
TRAINLOG_COLOR_ACCENT
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
mvprintw(
|
|
||||||
mode_row,
|
|
||||||
decorated ? 5 : 4,
|
|
||||||
"Mode : %-12s Charge : %-10s Repos : %s",
|
|
||||||
exercise->tracking_mode ==
|
|
||||||
TRAINLOG_TRACKING_REPS
|
|
||||||
? "répétitions"
|
|
||||||
: "durée",
|
|
||||||
session_detail_load_label(
|
|
||||||
exercise->load_mode
|
|
||||||
),
|
|
||||||
rest_text
|
|
||||||
);
|
|
||||||
|
|
||||||
if (exercise->tracking_mode ==
|
|
||||||
TRAINLOG_TRACKING_REPS) {
|
|
||||||
mvprintw(
|
|
||||||
target_row,
|
|
||||||
decorated ? 5 : 4,
|
|
||||||
"Cible : %d série(s) × %d reps",
|
|
||||||
exercise->target_sets,
|
|
||||||
exercise->target_reps
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
mvprintw(
|
|
||||||
target_row,
|
|
||||||
decorated ? 5 : 4,
|
|
||||||
"Cible : %d série(s) × %s",
|
|
||||||
exercise->target_sets,
|
|
||||||
target_duration_text
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exercise->has_target_weight != 0) {
|
|
||||||
mvprintw(
|
|
||||||
weight_row,
|
|
||||||
decorated ? 5 : 4,
|
|
||||||
"Charge cible : %.1f kg",
|
|
||||||
exercise->target_weight_kg
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
mvprintw(
|
|
||||||
weight_row,
|
|
||||||
decorated ? 5 : 4,
|
|
||||||
"Charge cible : —"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
attron(
|
|
||||||
A_BOLD |
|
|
||||||
trainlog_theme_attribute(
|
|
||||||
TRAINLOG_COLOR_SUCCESS
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
mvprintw(
|
|
||||||
actual_row,
|
|
||||||
decorated ? 5 : 4,
|
|
||||||
"Réalisé : %zu série(s)",
|
|
||||||
exercise->actual_set_count
|
|
||||||
);
|
|
||||||
|
|
||||||
attroff(
|
|
||||||
A_BOLD |
|
|
||||||
trainlog_theme_attribute(
|
|
||||||
TRAINLOG_COLOR_SUCCESS
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
mvprintw(
|
|
||||||
summary_row,
|
|
||||||
decorated ? 5 : 4,
|
|
||||||
"%.*s",
|
|
||||||
COLS - 10,
|
|
||||||
exercise->actual_summary
|
|
||||||
);
|
|
||||||
|
|
||||||
if (exercise->load_mode ==
|
|
||||||
TRAINLOG_LOAD_ASSISTANCE) {
|
|
||||||
attron(
|
attron(
|
||||||
|
A_BOLD |
|
||||||
trainlog_theme_attribute(
|
trainlog_theme_attribute(
|
||||||
TRAINLOG_COLOR_WARNING
|
TRAINLOG_COLOR_ACCENT
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
mvprintw(
|
mvprintw(
|
||||||
warning_row,
|
title_row,
|
||||||
decorated ? 5 : 4,
|
decorated ? 5 : 4,
|
||||||
"Assistance : plus de kg = davantage d'aide."
|
"Exercice %zu/%zu — %s",
|
||||||
|
selected + 1U,
|
||||||
|
count,
|
||||||
|
exercise->name
|
||||||
);
|
);
|
||||||
|
|
||||||
attroff(
|
attroff(
|
||||||
|
A_BOLD |
|
||||||
trainlog_theme_attribute(
|
trainlog_theme_attribute(
|
||||||
TRAINLOG_COLOR_WARNING
|
TRAINLOG_COLOR_ACCENT
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
mvprintw(
|
||||||
|
mode_row,
|
||||||
|
decorated ? 5 : 4,
|
||||||
|
"Mode : continu"
|
||||||
|
);
|
||||||
|
|
||||||
|
mvprintw(
|
||||||
|
row++,
|
||||||
|
decorated ? 5 : 4,
|
||||||
|
"Durée : %s",
|
||||||
|
duration_text
|
||||||
|
);
|
||||||
|
|
||||||
|
if (exercise->has_continuous_speed != 0) {
|
||||||
|
mvprintw(
|
||||||
|
row++,
|
||||||
|
decorated ? 5 : 4,
|
||||||
|
"Vitesse : %.1f km/h",
|
||||||
|
exercise->continuous_speed_kmh
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exercise->has_continuous_distance != 0) {
|
||||||
|
mvprintw(
|
||||||
|
row++,
|
||||||
|
decorated ? 5 : 4,
|
||||||
|
"Distance : %.2f km",
|
||||||
|
exercise->continuous_distance_km
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
attron(
|
||||||
|
A_BOLD |
|
||||||
|
trainlog_theme_attribute(
|
||||||
|
TRAINLOG_COLOR_SUCCESS
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
mvprintw(
|
||||||
|
row + 1,
|
||||||
|
decorated ? 5 : 4,
|
||||||
|
"Réalisé : activité continue"
|
||||||
|
);
|
||||||
|
|
||||||
|
attroff(
|
||||||
|
A_BOLD |
|
||||||
|
trainlog_theme_attribute(
|
||||||
|
TRAINLOG_COLOR_SUCCESS
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
char rest_text[64];
|
||||||
|
char target_duration_text[64];
|
||||||
|
|
||||||
|
int title_row =
|
||||||
|
decorated ? 16 : 6;
|
||||||
|
|
||||||
|
int mode_row =
|
||||||
|
decorated ? 18 : 8;
|
||||||
|
|
||||||
|
int target_row =
|
||||||
|
decorated ? 20 : 10;
|
||||||
|
|
||||||
|
int weight_row =
|
||||||
|
decorated ? 21 : 11;
|
||||||
|
|
||||||
|
int actual_row =
|
||||||
|
decorated ? 23 : 13;
|
||||||
|
|
||||||
|
int summary_row =
|
||||||
|
decorated ? 25 : 15;
|
||||||
|
|
||||||
|
int warning_row =
|
||||||
|
decorated ? 27 : 17;
|
||||||
|
|
||||||
|
if (trainlog_duration_format(
|
||||||
|
exercise->rest_seconds,
|
||||||
|
rest_text,
|
||||||
|
sizeof(rest_text)
|
||||||
|
) != TRAINLOG_STATUS_OK) {
|
||||||
|
(void)snprintf(
|
||||||
|
rest_text,
|
||||||
|
sizeof(rest_text),
|
||||||
|
"%ds",
|
||||||
|
exercise->rest_seconds
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
target_duration_text[0] = '\0';
|
||||||
|
|
||||||
|
if (exercise->tracking_mode ==
|
||||||
|
TRAINLOG_TRACKING_DURATION) {
|
||||||
|
(void)trainlog_duration_format(
|
||||||
|
exercise->target_duration_seconds,
|
||||||
|
target_duration_text,
|
||||||
|
sizeof(target_duration_text)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
attron(
|
||||||
|
A_BOLD |
|
||||||
|
trainlog_theme_attribute(
|
||||||
|
TRAINLOG_COLOR_ACCENT
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
mvprintw(
|
||||||
|
title_row,
|
||||||
|
decorated ? 5 : 4,
|
||||||
|
"Exercice %zu/%zu — %s",
|
||||||
|
selected + 1U,
|
||||||
|
count,
|
||||||
|
exercise->name
|
||||||
|
);
|
||||||
|
|
||||||
|
attroff(
|
||||||
|
A_BOLD |
|
||||||
|
trainlog_theme_attribute(
|
||||||
|
TRAINLOG_COLOR_ACCENT
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
mvprintw(
|
||||||
|
mode_row,
|
||||||
|
decorated ? 5 : 4,
|
||||||
|
"Mode : %-12s Charge : %-10s Repos : %s",
|
||||||
|
exercise->tracking_mode ==
|
||||||
|
TRAINLOG_TRACKING_REPS
|
||||||
|
? "répétitions"
|
||||||
|
: "durée",
|
||||||
|
session_detail_load_label(
|
||||||
|
exercise->load_mode
|
||||||
|
),
|
||||||
|
rest_text
|
||||||
|
);
|
||||||
|
|
||||||
|
if (exercise->tracking_mode ==
|
||||||
|
TRAINLOG_TRACKING_REPS) {
|
||||||
|
mvprintw(
|
||||||
|
target_row,
|
||||||
|
decorated ? 5 : 4,
|
||||||
|
"Cible : %d série(s) × %d reps",
|
||||||
|
exercise->target_sets,
|
||||||
|
exercise->target_reps
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
mvprintw(
|
||||||
|
target_row,
|
||||||
|
decorated ? 5 : 4,
|
||||||
|
"Cible : %d série(s) × %s",
|
||||||
|
exercise->target_sets,
|
||||||
|
target_duration_text
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exercise->has_target_weight != 0) {
|
||||||
|
mvprintw(
|
||||||
|
weight_row,
|
||||||
|
decorated ? 5 : 4,
|
||||||
|
"Charge cible : %.1f kg",
|
||||||
|
exercise->target_weight_kg
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
mvprintw(
|
||||||
|
weight_row,
|
||||||
|
decorated ? 5 : 4,
|
||||||
|
"Charge cible : —"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
attron(
|
||||||
|
A_BOLD |
|
||||||
|
trainlog_theme_attribute(
|
||||||
|
TRAINLOG_COLOR_SUCCESS
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
mvprintw(
|
||||||
|
actual_row,
|
||||||
|
decorated ? 5 : 4,
|
||||||
|
"Réalisé : %zu série(s)",
|
||||||
|
exercise->actual_set_count
|
||||||
|
);
|
||||||
|
|
||||||
|
attroff(
|
||||||
|
A_BOLD |
|
||||||
|
trainlog_theme_attribute(
|
||||||
|
TRAINLOG_COLOR_SUCCESS
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
mvprintw(
|
||||||
|
summary_row,
|
||||||
|
decorated ? 5 : 4,
|
||||||
|
"%.*s",
|
||||||
|
COLS - 10,
|
||||||
|
exercise->actual_summary
|
||||||
|
);
|
||||||
|
|
||||||
|
if (exercise->load_mode ==
|
||||||
|
TRAINLOG_LOAD_ASSISTANCE) {
|
||||||
|
attron(
|
||||||
|
trainlog_theme_attribute(
|
||||||
|
TRAINLOG_COLOR_WARNING
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
mvprintw(
|
||||||
|
warning_row,
|
||||||
|
decorated ? 5 : 4,
|
||||||
|
"Assistance : plus de kg = davantage d'aide."
|
||||||
|
);
|
||||||
|
|
||||||
|
attroff(
|
||||||
|
trainlog_theme_attribute(
|
||||||
|
TRAINLOG_COLOR_WARNING
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,100 @@ static bool test_blank_rejected(void)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool test_profiled_creation(void)
|
||||||
|
{
|
||||||
|
TrainlogDatabase *database = NULL;
|
||||||
|
TrainlogExercise walk;
|
||||||
|
TrainlogExercise exercises[4];
|
||||||
|
size_t count = 0U;
|
||||||
|
size_t index;
|
||||||
|
bool found = false;
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_open(
|
||||||
|
":memory:",
|
||||||
|
&database
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_catalog_create_exercise_profiled(
|
||||||
|
database,
|
||||||
|
"Marche",
|
||||||
|
TRAINLOG_TRACKING_DURATION,
|
||||||
|
TRAINLOG_RECORDING_CONTINUOUS,
|
||||||
|
TRAINLOG_EXERCISE_DATA_SPEED_KMH,
|
||||||
|
&walk
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
walk.recording_mode ==
|
||||||
|
TRAINLOG_RECORDING_CONTINUOUS
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
walk.tracking_mode ==
|
||||||
|
TRAINLOG_TRACKING_DURATION
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
walk.data_fields ==
|
||||||
|
TRAINLOG_EXERCISE_DATA_SPEED_KMH
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_catalog_create_exercise_profiled(
|
||||||
|
database,
|
||||||
|
"Profil invalide",
|
||||||
|
TRAINLOG_TRACKING_REPS,
|
||||||
|
TRAINLOG_RECORDING_CONTINUOUS,
|
||||||
|
0U,
|
||||||
|
&walk
|
||||||
|
) == TRAINLOG_STATUS_INVALID_ARGUMENT
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_list_exercises(
|
||||||
|
database,
|
||||||
|
exercises,
|
||||||
|
4U,
|
||||||
|
&count
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(count == 1U);
|
||||||
|
|
||||||
|
for (index = 0U; index < count; ++index) {
|
||||||
|
if (strcmp(
|
||||||
|
exercises[index].name,
|
||||||
|
"Marche"
|
||||||
|
) == 0) {
|
||||||
|
CHECK(
|
||||||
|
exercises[index].recording_mode ==
|
||||||
|
TRAINLOG_RECORDING_CONTINUOUS
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
exercises[index].tracking_mode ==
|
||||||
|
TRAINLOG_TRACKING_DURATION
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
exercises[index].data_fields ==
|
||||||
|
TRAINLOG_EXERCISE_DATA_SPEED_KMH
|
||||||
|
);
|
||||||
|
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CHECK(found);
|
||||||
|
|
||||||
|
trainlog_database_close(database);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
CHECK(test_normalization());
|
CHECK(test_normalization());
|
||||||
|
|
@ -80,5 +174,8 @@ int main(void)
|
||||||
CHECK(test_blank_rejected());
|
CHECK(test_blank_rejected());
|
||||||
(void)printf("PASS blank_rejected\n");
|
(void)printf("PASS blank_rejected\n");
|
||||||
|
|
||||||
|
CHECK(test_profiled_creation());
|
||||||
|
(void)printf("PASS profiled_creation\n");
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
168
tui/tests/test_continuous_detail.c
Normal file
168
tui/tests/test_continuous_detail.c
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
#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 1; \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
TrainlogDatabase *database = NULL;
|
||||||
|
TrainlogSessionExerciseInput exercise;
|
||||||
|
TrainlogSessionInput session;
|
||||||
|
TrainlogSessionSummary summary;
|
||||||
|
TrainlogPersistedExerciseDetail details[2];
|
||||||
|
size_t count = 0U;
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_open(
|
||||||
|
":memory:",
|
||||||
|
&database
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_insert_exercise_profiled(
|
||||||
|
database,
|
||||||
|
"ex_walk",
|
||||||
|
"Marche",
|
||||||
|
"marche",
|
||||||
|
TRAINLOG_TRACKING_DURATION,
|
||||||
|
TRAINLOG_RECORDING_CONTINUOUS,
|
||||||
|
TRAINLOG_EXERCISE_DATA_SPEED_KMH
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)memset(
|
||||||
|
&exercise,
|
||||||
|
0,
|
||||||
|
sizeof(exercise)
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
exercise.exercise_id,
|
||||||
|
sizeof(exercise.exercise_id),
|
||||||
|
"%s",
|
||||||
|
"ex_walk"
|
||||||
|
);
|
||||||
|
|
||||||
|
exercise.recording_mode =
|
||||||
|
TRAINLOG_RECORDING_CONTINUOUS;
|
||||||
|
|
||||||
|
exercise.data_fields =
|
||||||
|
TRAINLOG_EXERCISE_DATA_SPEED_KMH;
|
||||||
|
|
||||||
|
exercise.load_mode =
|
||||||
|
TRAINLOG_LOAD_NONE;
|
||||||
|
|
||||||
|
exercise.continuous_duration_seconds =
|
||||||
|
1800;
|
||||||
|
|
||||||
|
exercise.continuous_has_speed =
|
||||||
|
true;
|
||||||
|
|
||||||
|
exercise.continuous_speed_kmh =
|
||||||
|
5.8;
|
||||||
|
|
||||||
|
(void)memset(
|
||||||
|
&session,
|
||||||
|
0,
|
||||||
|
sizeof(session)
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
session.session_id,
|
||||||
|
sizeof(session.session_id),
|
||||||
|
"%s",
|
||||||
|
"se_walk_detail"
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
session.started_at,
|
||||||
|
sizeof(session.started_at),
|
||||||
|
"%s",
|
||||||
|
"2026-09-06T13:00:00+02:00"
|
||||||
|
);
|
||||||
|
|
||||||
|
session.exercises = &exercise;
|
||||||
|
session.exercise_count = 1U;
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_insert_session(
|
||||||
|
database,
|
||||||
|
&session
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_get_session_details(
|
||||||
|
database,
|
||||||
|
session.session_id,
|
||||||
|
&summary,
|
||||||
|
details,
|
||||||
|
2U,
|
||||||
|
&count
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(count == 1U);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
details[0].recording_mode ==
|
||||||
|
TRAINLOG_RECORDING_CONTINUOUS
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
details[0].continuous_duration_seconds ==
|
||||||
|
1800
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
details[0].has_continuous_speed != 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
details[0].continuous_speed_kmh >
|
||||||
|
5.79
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
details[0].continuous_speed_kmh <
|
||||||
|
5.81
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
details[0].actual_set_count ==
|
||||||
|
0U
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
strcmp(
|
||||||
|
details[0].actual_summary,
|
||||||
|
"activité continue"
|
||||||
|
) == 0
|
||||||
|
);
|
||||||
|
|
||||||
|
trainlog_database_close(
|
||||||
|
database
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)printf(
|
||||||
|
"PASS continuous_detail\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
275
tui/tests/test_continuous_session.c
Normal file
275
tui/tests/test_continuous_session.c
Normal file
|
|
@ -0,0 +1,275 @@
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <sqlite3.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 1; \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
char path[] =
|
||||||
|
"/tmp/trainlog-continuous-XXXXXX";
|
||||||
|
|
||||||
|
TrainlogDatabase *database = NULL;
|
||||||
|
TrainlogSessionExerciseInput exercise;
|
||||||
|
TrainlogSessionInput session;
|
||||||
|
sqlite3 *raw = NULL;
|
||||||
|
sqlite3_stmt *statement = NULL;
|
||||||
|
int fd;
|
||||||
|
|
||||||
|
fd = mkstemp(path);
|
||||||
|
CHECK(fd >= 0);
|
||||||
|
CHECK(close(fd) == 0);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_open(
|
||||||
|
path,
|
||||||
|
&database
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_insert_exercise_profiled(
|
||||||
|
database,
|
||||||
|
"ex_walk",
|
||||||
|
"Marche",
|
||||||
|
"marche",
|
||||||
|
TRAINLOG_TRACKING_DURATION,
|
||||||
|
TRAINLOG_RECORDING_CONTINUOUS,
|
||||||
|
TRAINLOG_EXERCISE_DATA_SPEED_KMH
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)memset(
|
||||||
|
&exercise,
|
||||||
|
0,
|
||||||
|
sizeof(exercise)
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
exercise.exercise_id,
|
||||||
|
sizeof(exercise.exercise_id),
|
||||||
|
"%s",
|
||||||
|
"ex_walk"
|
||||||
|
);
|
||||||
|
|
||||||
|
exercise.recording_mode =
|
||||||
|
TRAINLOG_RECORDING_CONTINUOUS;
|
||||||
|
|
||||||
|
exercise.data_fields =
|
||||||
|
TRAINLOG_EXERCISE_DATA_SPEED_KMH;
|
||||||
|
|
||||||
|
exercise.load_mode =
|
||||||
|
TRAINLOG_LOAD_NONE;
|
||||||
|
|
||||||
|
exercise.continuous_duration_seconds =
|
||||||
|
2700;
|
||||||
|
|
||||||
|
exercise.continuous_has_speed =
|
||||||
|
true;
|
||||||
|
|
||||||
|
exercise.continuous_speed_kmh =
|
||||||
|
5.8;
|
||||||
|
|
||||||
|
(void)memset(
|
||||||
|
&session,
|
||||||
|
0,
|
||||||
|
sizeof(session)
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
session.session_id,
|
||||||
|
sizeof(session.session_id),
|
||||||
|
"%s",
|
||||||
|
"se_walk"
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
session.started_at,
|
||||||
|
sizeof(session.started_at),
|
||||||
|
"%s",
|
||||||
|
"2026-09-06T12:00:00+02:00"
|
||||||
|
);
|
||||||
|
|
||||||
|
session.exercises = &exercise;
|
||||||
|
session.exercise_count = 1U;
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_insert_session(
|
||||||
|
database,
|
||||||
|
&session
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
trainlog_database_close(database);
|
||||||
|
database = NULL;
|
||||||
|
|
||||||
|
CHECK(sqlite3_open(path, &raw) == SQLITE_OK);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
sqlite3_prepare_v2(
|
||||||
|
raw,
|
||||||
|
"SELECT recording_mode, data_fields, "
|
||||||
|
"target_sets, target_reps, "
|
||||||
|
"target_duration_seconds "
|
||||||
|
"FROM session_exercises "
|
||||||
|
"WHERE id = 1;",
|
||||||
|
-1,
|
||||||
|
&statement,
|
||||||
|
NULL
|
||||||
|
) == SQLITE_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(sqlite3_step(statement) == SQLITE_ROW);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
strcmp(
|
||||||
|
(const char *)
|
||||||
|
sqlite3_column_text(
|
||||||
|
statement,
|
||||||
|
0
|
||||||
|
),
|
||||||
|
"continuous"
|
||||||
|
) == 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
sqlite3_column_int(
|
||||||
|
statement,
|
||||||
|
1
|
||||||
|
) ==
|
||||||
|
(int)TRAINLOG_EXERCISE_DATA_SPEED_KMH
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
sqlite3_column_type(
|
||||||
|
statement,
|
||||||
|
2
|
||||||
|
) == SQLITE_NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
sqlite3_column_type(
|
||||||
|
statement,
|
||||||
|
3
|
||||||
|
) == SQLITE_NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
sqlite3_column_type(
|
||||||
|
statement,
|
||||||
|
4
|
||||||
|
) == SQLITE_NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
sqlite3_finalize(
|
||||||
|
statement
|
||||||
|
) == SQLITE_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
statement = NULL;
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
sqlite3_prepare_v2(
|
||||||
|
raw,
|
||||||
|
"SELECT duration_seconds, "
|
||||||
|
"speed_kmh, distance_km "
|
||||||
|
"FROM continuous_activity "
|
||||||
|
"WHERE session_exercise_row_id = 1;",
|
||||||
|
-1,
|
||||||
|
&statement,
|
||||||
|
NULL
|
||||||
|
) == SQLITE_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(sqlite3_step(statement) == SQLITE_ROW);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
sqlite3_column_int(
|
||||||
|
statement,
|
||||||
|
0
|
||||||
|
) == 2700
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
sqlite3_column_double(
|
||||||
|
statement,
|
||||||
|
1
|
||||||
|
) > 5.79
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
sqlite3_column_double(
|
||||||
|
statement,
|
||||||
|
1
|
||||||
|
) < 5.81
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
sqlite3_column_type(
|
||||||
|
statement,
|
||||||
|
2
|
||||||
|
) == SQLITE_NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
sqlite3_finalize(
|
||||||
|
statement
|
||||||
|
) == SQLITE_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
statement = NULL;
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
sqlite3_prepare_v2(
|
||||||
|
raw,
|
||||||
|
"SELECT COUNT(*) "
|
||||||
|
"FROM performed_sets;",
|
||||||
|
-1,
|
||||||
|
&statement,
|
||||||
|
NULL
|
||||||
|
) == SQLITE_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(sqlite3_step(statement) == SQLITE_ROW);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
sqlite3_column_int(
|
||||||
|
statement,
|
||||||
|
0
|
||||||
|
) == 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
sqlite3_finalize(
|
||||||
|
statement
|
||||||
|
) == SQLITE_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(sqlite3_close(raw) == SQLITE_OK);
|
||||||
|
CHECK(unlink(path) == 0);
|
||||||
|
|
||||||
|
(void)printf(
|
||||||
|
"PASS continuous_session\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
288
tui/tests/test_exercise_profile_schema.c
Normal file
288
tui/tests/test_exercise_profile_schema.c
Normal file
|
|
@ -0,0 +1,288 @@
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <sqlite3.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_profile_roundtrip(void)
|
||||||
|
{
|
||||||
|
TrainlogDatabase *database = NULL;
|
||||||
|
TrainlogExercise exercises[4];
|
||||||
|
size_t count = 0U;
|
||||||
|
size_t index;
|
||||||
|
bool found_walk = false;
|
||||||
|
bool found_legacy = false;
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_open(
|
||||||
|
":memory:",
|
||||||
|
&database
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_insert_exercise(
|
||||||
|
database,
|
||||||
|
"ex_legacy",
|
||||||
|
"Presse",
|
||||||
|
"presse",
|
||||||
|
TRAINLOG_TRACKING_REPS
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_insert_exercise_profiled(
|
||||||
|
database,
|
||||||
|
"ex_walk",
|
||||||
|
"Marche",
|
||||||
|
"marche",
|
||||||
|
TRAINLOG_TRACKING_DURATION,
|
||||||
|
TRAINLOG_RECORDING_CONTINUOUS,
|
||||||
|
TRAINLOG_EXERCISE_DATA_SPEED_KMH
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_insert_exercise_profiled(
|
||||||
|
database,
|
||||||
|
"ex_invalid",
|
||||||
|
"Invalid",
|
||||||
|
"invalid",
|
||||||
|
TRAINLOG_TRACKING_REPS,
|
||||||
|
TRAINLOG_RECORDING_CONTINUOUS,
|
||||||
|
0U
|
||||||
|
) == TRAINLOG_STATUS_INVALID_ARGUMENT
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_insert_exercise_profiled(
|
||||||
|
database,
|
||||||
|
"ex_unknown",
|
||||||
|
"Unknown",
|
||||||
|
"unknown",
|
||||||
|
TRAINLOG_TRACKING_DURATION,
|
||||||
|
TRAINLOG_RECORDING_CONTINUOUS,
|
||||||
|
UINT32_C(8)
|
||||||
|
) == TRAINLOG_STATUS_INVALID_ARGUMENT
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_list_exercises(
|
||||||
|
database,
|
||||||
|
exercises,
|
||||||
|
4U,
|
||||||
|
&count
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(count == 2U);
|
||||||
|
|
||||||
|
for (index = 0U; index < count; ++index) {
|
||||||
|
if (strcmp(exercises[index].exercise_id, "ex_walk") == 0) {
|
||||||
|
CHECK(
|
||||||
|
exercises[index].recording_mode ==
|
||||||
|
TRAINLOG_RECORDING_CONTINUOUS
|
||||||
|
);
|
||||||
|
CHECK(
|
||||||
|
exercises[index].tracking_mode ==
|
||||||
|
TRAINLOG_TRACKING_DURATION
|
||||||
|
);
|
||||||
|
CHECK(
|
||||||
|
exercises[index].data_fields ==
|
||||||
|
TRAINLOG_EXERCISE_DATA_SPEED_KMH
|
||||||
|
);
|
||||||
|
found_walk = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(exercises[index].exercise_id, "ex_legacy") == 0) {
|
||||||
|
CHECK(
|
||||||
|
exercises[index].recording_mode ==
|
||||||
|
TRAINLOG_RECORDING_SETS
|
||||||
|
);
|
||||||
|
CHECK(exercises[index].data_fields == 0U);
|
||||||
|
found_legacy = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CHECK(found_walk);
|
||||||
|
CHECK(found_legacy);
|
||||||
|
|
||||||
|
trainlog_database_close(database);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test_v2_to_v3_migration(void)
|
||||||
|
{
|
||||||
|
char path[] =
|
||||||
|
"/tmp/trainlog-schema-v2-profile-XXXXXX";
|
||||||
|
|
||||||
|
static const char *const V2_SQL =
|
||||||
|
"CREATE TABLE exercises ("
|
||||||
|
"id INTEGER PRIMARY KEY,"
|
||||||
|
"exercise_id TEXT NOT NULL UNIQUE,"
|
||||||
|
"name TEXT NOT NULL,"
|
||||||
|
"normalized_name TEXT NOT NULL UNIQUE,"
|
||||||
|
"tracking_mode TEXT NOT NULL"
|
||||||
|
");"
|
||||||
|
"CREATE TABLE sessions ("
|
||||||
|
"id INTEGER PRIMARY KEY,"
|
||||||
|
"session_id TEXT NOT NULL UNIQUE,"
|
||||||
|
"started_at TEXT NOT NULL,"
|
||||||
|
"ended_at TEXT,"
|
||||||
|
"session_type TEXT NOT NULL DEFAULT 'training',"
|
||||||
|
"notes TEXT"
|
||||||
|
");"
|
||||||
|
"CREATE TABLE session_exercises ("
|
||||||
|
"id INTEGER PRIMARY KEY,"
|
||||||
|
"session_row_id INTEGER NOT NULL REFERENCES sessions(id),"
|
||||||
|
"exercise_row_id INTEGER NOT NULL REFERENCES exercises(id),"
|
||||||
|
"position INTEGER NOT NULL,"
|
||||||
|
"load_mode TEXT NOT NULL,"
|
||||||
|
"rest_seconds INTEGER NOT NULL,"
|
||||||
|
"target_sets INTEGER NOT NULL,"
|
||||||
|
"target_reps INTEGER,"
|
||||||
|
"target_duration_seconds INTEGER,"
|
||||||
|
"target_weight_kg REAL,"
|
||||||
|
"notes TEXT"
|
||||||
|
");"
|
||||||
|
"INSERT INTO exercises("
|
||||||
|
"exercise_id, name, normalized_name, tracking_mode"
|
||||||
|
") VALUES('ex_old', 'Ancien', 'ancien', 'duration');"
|
||||||
|
"INSERT INTO sessions("
|
||||||
|
"session_id, started_at, session_type"
|
||||||
|
") VALUES('se_old', '2026-09-01T10:00:00+02:00', 'training');"
|
||||||
|
"INSERT INTO session_exercises("
|
||||||
|
"session_row_id, exercise_row_id, position, load_mode,"
|
||||||
|
"rest_seconds, target_sets, target_duration_seconds"
|
||||||
|
") VALUES(1, 1, 0, 'none', 0, 1, 60);"
|
||||||
|
"PRAGMA user_version = 2;";
|
||||||
|
|
||||||
|
sqlite3 *raw = NULL;
|
||||||
|
sqlite3_stmt *statement = NULL;
|
||||||
|
TrainlogDatabase *database = NULL;
|
||||||
|
int fd;
|
||||||
|
int version = 0;
|
||||||
|
|
||||||
|
fd = mkstemp(path);
|
||||||
|
CHECK(fd >= 0);
|
||||||
|
CHECK(close(fd) == 0);
|
||||||
|
|
||||||
|
CHECK(sqlite3_open(path, &raw) == SQLITE_OK);
|
||||||
|
CHECK(
|
||||||
|
sqlite3_exec(raw, V2_SQL, NULL, NULL, NULL) ==
|
||||||
|
SQLITE_OK
|
||||||
|
);
|
||||||
|
CHECK(sqlite3_close(raw) == SQLITE_OK);
|
||||||
|
raw = NULL;
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_open(path, &database) ==
|
||||||
|
TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_schema_version(
|
||||||
|
database,
|
||||||
|
&version
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(version == 3);
|
||||||
|
|
||||||
|
trainlog_database_close(database);
|
||||||
|
database = NULL;
|
||||||
|
|
||||||
|
CHECK(sqlite3_open(path, &raw) == SQLITE_OK);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
sqlite3_prepare_v2(
|
||||||
|
raw,
|
||||||
|
"SELECT recording_mode, data_fields "
|
||||||
|
"FROM exercises WHERE exercise_id='ex_old';",
|
||||||
|
-1,
|
||||||
|
&statement,
|
||||||
|
NULL
|
||||||
|
) == SQLITE_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(sqlite3_step(statement) == SQLITE_ROW);
|
||||||
|
CHECK(
|
||||||
|
strcmp(
|
||||||
|
(const char *)sqlite3_column_text(statement, 0),
|
||||||
|
"sets"
|
||||||
|
) == 0
|
||||||
|
);
|
||||||
|
CHECK(sqlite3_column_int64(statement, 1) == 0);
|
||||||
|
CHECK(sqlite3_finalize(statement) == SQLITE_OK);
|
||||||
|
statement = NULL;
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
sqlite3_prepare_v2(
|
||||||
|
raw,
|
||||||
|
"SELECT recording_mode, data_fields "
|
||||||
|
"FROM session_exercises WHERE id=1;",
|
||||||
|
-1,
|
||||||
|
&statement,
|
||||||
|
NULL
|
||||||
|
) == SQLITE_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(sqlite3_step(statement) == SQLITE_ROW);
|
||||||
|
CHECK(
|
||||||
|
strcmp(
|
||||||
|
(const char *)sqlite3_column_text(statement, 0),
|
||||||
|
"sets"
|
||||||
|
) == 0
|
||||||
|
);
|
||||||
|
CHECK(sqlite3_column_int64(statement, 1) == 0);
|
||||||
|
CHECK(sqlite3_finalize(statement) == SQLITE_OK);
|
||||||
|
statement = NULL;
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
sqlite3_prepare_v2(
|
||||||
|
raw,
|
||||||
|
"SELECT name FROM sqlite_master "
|
||||||
|
"WHERE type='table' AND name='continuous_activity';",
|
||||||
|
-1,
|
||||||
|
&statement,
|
||||||
|
NULL
|
||||||
|
) == SQLITE_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(sqlite3_step(statement) == SQLITE_ROW);
|
||||||
|
CHECK(sqlite3_finalize(statement) == SQLITE_OK);
|
||||||
|
|
||||||
|
CHECK(sqlite3_close(raw) == SQLITE_OK);
|
||||||
|
raw = NULL;
|
||||||
|
|
||||||
|
CHECK(unlink(path) == 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
CHECK(test_profile_roundtrip());
|
||||||
|
CHECK(test_v2_to_v3_migration());
|
||||||
|
|
||||||
|
(void)printf("PASS exercise_profile_schema\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* @file test_session_type_schema.c
|
* @file test_session_type_schema.c
|
||||||
* @brief Schema v2 and session type persistence tests.
|
* @brief Historical schema migration and session type persistence tests.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
@ -141,6 +141,14 @@ static bool test_v1_to_v2_migration(void)
|
||||||
"/tmp/trainlog-schema-v1-XXXXXX";
|
"/tmp/trainlog-schema-v1-XXXXXX";
|
||||||
|
|
||||||
static const char *const V1_SQL =
|
static const char *const V1_SQL =
|
||||||
|
"CREATE TABLE exercises ("
|
||||||
|
"id INTEGER PRIMARY KEY,"
|
||||||
|
"exercise_id TEXT NOT NULL UNIQUE,"
|
||||||
|
"name TEXT NOT NULL,"
|
||||||
|
"normalized_name TEXT NOT NULL UNIQUE,"
|
||||||
|
"tracking_mode TEXT NOT NULL"
|
||||||
|
" CHECK (tracking_mode IN ('reps', 'duration'))"
|
||||||
|
");"
|
||||||
"CREATE TABLE sessions ("
|
"CREATE TABLE sessions ("
|
||||||
"id INTEGER PRIMARY KEY,"
|
"id INTEGER PRIMARY KEY,"
|
||||||
"session_id TEXT NOT NULL UNIQUE,"
|
"session_id TEXT NOT NULL UNIQUE,"
|
||||||
|
|
@ -148,6 +156,48 @@ static bool test_v1_to_v2_migration(void)
|
||||||
"ended_at TEXT,"
|
"ended_at TEXT,"
|
||||||
"notes TEXT"
|
"notes TEXT"
|
||||||
");"
|
");"
|
||||||
|
"CREATE TABLE session_exercises ("
|
||||||
|
"id INTEGER PRIMARY KEY,"
|
||||||
|
"session_row_id INTEGER NOT NULL REFERENCES sessions(id),"
|
||||||
|
"exercise_row_id INTEGER NOT NULL REFERENCES exercises(id),"
|
||||||
|
"position INTEGER NOT NULL,"
|
||||||
|
"load_mode TEXT NOT NULL,"
|
||||||
|
"rest_seconds INTEGER NOT NULL,"
|
||||||
|
"target_sets INTEGER NOT NULL,"
|
||||||
|
"target_reps INTEGER,"
|
||||||
|
"target_duration_seconds INTEGER,"
|
||||||
|
"target_weight_kg REAL,"
|
||||||
|
"notes TEXT"
|
||||||
|
");"
|
||||||
|
"CREATE TABLE performed_sets ("
|
||||||
|
"id INTEGER PRIMARY KEY,"
|
||||||
|
"session_exercise_row_id INTEGER NOT NULL REFERENCES session_exercises(id),"
|
||||||
|
"position INTEGER NOT NULL,"
|
||||||
|
"reps INTEGER,"
|
||||||
|
"duration_seconds INTEGER,"
|
||||||
|
"weight_kg REAL"
|
||||||
|
");"
|
||||||
|
"CREATE TABLE body_observations ("
|
||||||
|
"id INTEGER PRIMARY KEY,"
|
||||||
|
"observation_id TEXT NOT NULL UNIQUE,"
|
||||||
|
"observed_at TEXT NOT NULL,"
|
||||||
|
"session_row_id INTEGER UNIQUE REFERENCES sessions(id),"
|
||||||
|
"body_weight_kg REAL,"
|
||||||
|
"neck_cm REAL,"
|
||||||
|
"shoulders_cm REAL,"
|
||||||
|
"chest_cm REAL,"
|
||||||
|
"waist_cm REAL,"
|
||||||
|
"hips_cm REAL,"
|
||||||
|
"left_arm_cm REAL,"
|
||||||
|
"right_arm_cm REAL,"
|
||||||
|
"left_forearm_cm REAL,"
|
||||||
|
"right_forearm_cm REAL,"
|
||||||
|
"left_thigh_cm REAL,"
|
||||||
|
"right_thigh_cm REAL,"
|
||||||
|
"left_calf_cm REAL,"
|
||||||
|
"right_calf_cm REAL,"
|
||||||
|
"notes TEXT"
|
||||||
|
");"
|
||||||
"INSERT INTO sessions("
|
"INSERT INTO sessions("
|
||||||
"session_id, started_at, ended_at, notes"
|
"session_id, started_at, ended_at, notes"
|
||||||
") VALUES("
|
") VALUES("
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue