Add measured max tracking and working loads
This commit is contained in:
parent
e68b6cdc14
commit
e2604131d4
21 changed files with 1859 additions and 55 deletions
16
CHANGELOG.md
16
CHANGELOG.md
|
|
@ -84,3 +84,19 @@ ANDROID_SYNC_RECEIPT=PASS
|
||||||
TUI_SYNC_LOG_SHOW=PASS
|
TUI_SYNC_LOG_SHOW=PASS
|
||||||
BIDIRECTIONAL_SYNC_V1=PASS
|
BIDIRECTIONAL_SYNC_V1=PASS
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Measured max v1
|
||||||
|
|
||||||
|
Added:
|
||||||
|
|
||||||
|
- explicit measured-max derivation from `max_test` sessions only;
|
||||||
|
- newest successful measured result and same-mode historical record;
|
||||||
|
- dedicated TUI measured-max history and graph;
|
||||||
|
- external working-load calculations at 60/70/80/90%;
|
||||||
|
- selectable 0.5/1/2.5/5.0 kg working-load rounding;
|
||||||
|
- direction-aware assistance measured-max semantics;
|
||||||
|
- Android `Entraînement` / `Test max` session selection;
|
||||||
|
- Android history/detail max-test identification;
|
||||||
|
- measured-max regression coverage.
|
||||||
|
|
||||||
|
No estimated 1RM, schema v6, or frozen Trainlog JSON v1 change was introduced.
|
||||||
|
|
|
||||||
22
README.md
22
README.md
|
|
@ -29,7 +29,7 @@ ANDROID_TRIGGERED_SYNC=PASS
|
||||||
ANDROID_SYNC_RECEIPT=PASS
|
ANDROID_SYNC_RECEIPT=PASS
|
||||||
BIDIRECTIONAL_SYNC_V1=PASS
|
BIDIRECTIONAL_SYNC_V1=PASS
|
||||||
|
|
||||||
DESKTOP_TESTS=19/19 PASS
|
DESKTOP_TESTS=20/20 PASS
|
||||||
ANDROID_BUILD=PASS
|
ANDROID_BUILD=PASS
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -187,3 +187,23 @@ runs, a receipt is returned to Android, and the PC catalog is applied locally.
|
||||||
- no fake data representation to force incompatible models together;
|
- no fake data representation to force incompatible models together;
|
||||||
- strict compiler warnings;
|
- strict compiler warnings;
|
||||||
- documentation and tests are part of feature completion.
|
- documentation and tests are part of feature completion.
|
||||||
|
|
||||||
|
## Measured max
|
||||||
|
|
||||||
|
Explicit `max_test` sessions are the only source of measured maxima.
|
||||||
|
|
||||||
|
Ordinary training best sets remain ordinary performance even when they exceed a
|
||||||
|
previous max-test result.
|
||||||
|
|
||||||
|
The desktop exercise catalog exposes a separate measured-max view with current
|
||||||
|
result, same-mode record, test history, a dedicated graph, and 60/70/80/90%
|
||||||
|
working loads for external resistance. Working loads are rounded to a selectable
|
||||||
|
practical increment and are not calculated for assistance.
|
||||||
|
|
||||||
|
Android can explicitly save a session as `Entraînement` or `Test max`.
|
||||||
|
|
||||||
|
```text
|
||||||
|
MEASURED_MAX_V1=PASS
|
||||||
|
WORKING_LOAD_PERCENTAGES=PASS
|
||||||
|
ANDROID_MAX_TEST_SESSION=PASS
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import com.labfytools.trainlog.model.SessionSummary
|
||||||
import com.labfytools.trainlog.model.SessionDetail
|
import com.labfytools.trainlog.model.SessionDetail
|
||||||
import com.labfytools.trainlog.model.SessionExerciseDetail
|
import com.labfytools.trainlog.model.SessionExerciseDetail
|
||||||
import com.labfytools.trainlog.model.SessionSetDraft
|
import com.labfytools.trainlog.model.SessionSetDraft
|
||||||
|
import com.labfytools.trainlog.model.SessionType
|
||||||
import com.labfytools.trainlog.model.TrackingMode
|
import com.labfytools.trainlog.model.TrackingMode
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
@ -296,7 +297,8 @@ class TrainlogRepository(
|
||||||
|
|
||||||
put(
|
put(
|
||||||
"session_type",
|
"session_type",
|
||||||
"training"
|
draft.sessionType
|
||||||
|
.wireValue
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -469,6 +471,7 @@ class TrainlogRepository(
|
||||||
SELECT
|
SELECT
|
||||||
s.session_id,
|
s.session_id,
|
||||||
s.started_at,
|
s.started_at,
|
||||||
|
s.session_type,
|
||||||
COUNT(se.id)
|
COUNT(se.id)
|
||||||
FROM sessions AS s
|
FROM sessions AS s
|
||||||
LEFT JOIN session_exercises AS se
|
LEFT JOIN session_exercises AS se
|
||||||
|
|
@ -488,7 +491,11 @@ class TrainlogRepository(
|
||||||
startedAt =
|
startedAt =
|
||||||
cursor.getString(1),
|
cursor.getString(1),
|
||||||
exerciseCount =
|
exerciseCount =
|
||||||
cursor.getInt(2),
|
cursor.getInt(3),
|
||||||
|
sessionType =
|
||||||
|
SessionType.fromWire(
|
||||||
|
cursor.getString(2)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1136,6 +1143,7 @@ class TrainlogRepository(
|
||||||
SELECT
|
SELECT
|
||||||
s.session_id,
|
s.session_id,
|
||||||
s.started_at,
|
s.started_at,
|
||||||
|
s.session_type,
|
||||||
COUNT(se.id)
|
COUNT(se.id)
|
||||||
FROM sessions AS s
|
FROM sessions AS s
|
||||||
LEFT JOIN session_exercises AS se
|
LEFT JOIN session_exercises AS se
|
||||||
|
|
@ -1154,7 +1162,11 @@ class TrainlogRepository(
|
||||||
startedAt =
|
startedAt =
|
||||||
cursor.getString(1),
|
cursor.getString(1),
|
||||||
exerciseCount =
|
exerciseCount =
|
||||||
cursor.getInt(2),
|
cursor.getInt(3),
|
||||||
|
sessionType =
|
||||||
|
SessionType.fromWire(
|
||||||
|
cursor.getString(2)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} ?: return null
|
} ?: return null
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,23 @@
|
||||||
package com.labfytools.trainlog.model
|
package com.labfytools.trainlog.model
|
||||||
|
|
||||||
|
enum class SessionType(
|
||||||
|
val wireValue: String,
|
||||||
|
) {
|
||||||
|
TRAINING("training"),
|
||||||
|
MAX_TEST("max_test");
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromWire(
|
||||||
|
value: String,
|
||||||
|
): SessionType =
|
||||||
|
if (value == "max_test") {
|
||||||
|
MAX_TEST
|
||||||
|
} else {
|
||||||
|
TRAINING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class SessionSetDraft(
|
data class SessionSetDraft(
|
||||||
val reps: Int = 0,
|
val reps: Int = 0,
|
||||||
val durationSeconds: Int = 0,
|
val durationSeconds: Int = 0,
|
||||||
|
|
@ -15,12 +33,14 @@ data class SessionExerciseDraft(
|
||||||
|
|
||||||
data class SessionDraft(
|
data class SessionDraft(
|
||||||
val exercises: List<SessionExerciseDraft>,
|
val exercises: List<SessionExerciseDraft>,
|
||||||
|
val sessionType: SessionType = SessionType.TRAINING,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class SessionSummary(
|
data class SessionSummary(
|
||||||
val sessionId: String,
|
val sessionId: String,
|
||||||
val startedAt: String,
|
val startedAt: String,
|
||||||
val exerciseCount: Int,
|
val exerciseCount: Int,
|
||||||
|
val sessionType: SessionType = SessionType.TRAINING,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class SessionExerciseDetail(
|
data class SessionExerciseDetail(
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package com.labfytools.trainlog.ui
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import com.labfytools.trainlog.data.TrainlogRepository
|
import com.labfytools.trainlog.data.TrainlogRepository
|
||||||
|
import com.labfytools.trainlog.model.SessionType
|
||||||
import com.labfytools.trainlog.ui.theme.LocalTrainlogColors
|
import com.labfytools.trainlog.ui.theme.LocalTrainlogColors
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -49,12 +50,25 @@ fun HistoryScreen(
|
||||||
session.startedAt
|
session.startedAt
|
||||||
),
|
),
|
||||||
description =
|
description =
|
||||||
|
sessionTypeLabel(
|
||||||
|
session.sessionType
|
||||||
|
) +
|
||||||
|
" · " +
|
||||||
"${session.exerciseCount} exercice(s)",
|
"${session.exerciseCount} exercice(s)",
|
||||||
onClick = {
|
onClick = {
|
||||||
onOpenSession(
|
onOpenSession(
|
||||||
session.sessionId
|
session.sessionId
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
accent =
|
||||||
|
if (
|
||||||
|
session.sessionType ==
|
||||||
|
SessionType.MAX_TEST
|
||||||
|
) {
|
||||||
|
colors.warning
|
||||||
|
} else {
|
||||||
|
colors.accent
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -62,6 +76,18 @@ fun HistoryScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun sessionTypeLabel(
|
||||||
|
value: SessionType,
|
||||||
|
): String =
|
||||||
|
if (
|
||||||
|
value ==
|
||||||
|
SessionType.MAX_TEST
|
||||||
|
) {
|
||||||
|
"TEST MAX"
|
||||||
|
} else {
|
||||||
|
"ENTRAÎNEMENT"
|
||||||
|
}
|
||||||
|
|
||||||
internal fun formatStartedAt(
|
internal fun formatStartedAt(
|
||||||
value: String,
|
value: String,
|
||||||
): String {
|
): String {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import com.labfytools.trainlog.data.TrainlogRepository
|
||||||
import com.labfytools.trainlog.model.ExerciseDataFields
|
import com.labfytools.trainlog.model.ExerciseDataFields
|
||||||
import com.labfytools.trainlog.model.RecordingMode
|
import com.labfytools.trainlog.model.RecordingMode
|
||||||
import com.labfytools.trainlog.model.SessionExerciseDetail
|
import com.labfytools.trainlog.model.SessionExerciseDetail
|
||||||
|
import com.labfytools.trainlog.model.SessionType
|
||||||
import com.labfytools.trainlog.model.TrackingMode
|
import com.labfytools.trainlog.model.TrackingMode
|
||||||
import com.labfytools.trainlog.ui.theme.LocalTrainlogColors
|
import com.labfytools.trainlog.ui.theme.LocalTrainlogColors
|
||||||
|
|
||||||
|
|
@ -63,6 +64,25 @@ fun SessionDetailScreen(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
TrainlogInfo(
|
||||||
|
text =
|
||||||
|
"Type : " +
|
||||||
|
sessionTypeLabel(
|
||||||
|
detail.summary
|
||||||
|
.sessionType
|
||||||
|
),
|
||||||
|
color =
|
||||||
|
if (
|
||||||
|
detail.summary
|
||||||
|
.sessionType ==
|
||||||
|
SessionType.MAX_TEST
|
||||||
|
) {
|
||||||
|
colors.warning
|
||||||
|
} else {
|
||||||
|
colors.accent
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
TrainlogInfo(
|
TrainlogInfo(
|
||||||
"${detail.summary.exerciseCount} exercice(s)"
|
"${detail.summary.exerciseCount} exercice(s)"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package com.labfytools.trainlog.ui
|
package com.labfytools.trainlog.ui
|
||||||
|
|
||||||
|
/* TRAINLOG_ANDROID_MAX_TEST_SESSION_V1 */
|
||||||
|
|
||||||
/* TRAINLOG_ANDROID_SESSION_REMOVE */
|
/* TRAINLOG_ANDROID_SESSION_REMOVE */
|
||||||
|
|
||||||
/* TRAINLOG_VARIABLE_SET_REPS_V1 */
|
/* TRAINLOG_VARIABLE_SET_REPS_V1 */
|
||||||
|
|
@ -30,6 +32,7 @@ import com.labfytools.trainlog.model.RecordingMode
|
||||||
import com.labfytools.trainlog.model.SessionDraft
|
import com.labfytools.trainlog.model.SessionDraft
|
||||||
import com.labfytools.trainlog.model.SessionExerciseDraft
|
import com.labfytools.trainlog.model.SessionExerciseDraft
|
||||||
import com.labfytools.trainlog.model.SessionSetDraft
|
import com.labfytools.trainlog.model.SessionSetDraft
|
||||||
|
import com.labfytools.trainlog.model.SessionType
|
||||||
import com.labfytools.trainlog.model.TrackingMode
|
import com.labfytools.trainlog.model.TrackingMode
|
||||||
import com.labfytools.trainlog.ui.theme.LocalTrainlogColors
|
import com.labfytools.trainlog.ui.theme.LocalTrainlogColors
|
||||||
import com.labfytools.trainlog.ui.theme.TrainlogTypography
|
import com.labfytools.trainlog.ui.theme.TrainlogTypography
|
||||||
|
|
@ -70,6 +73,13 @@ fun SessionScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var sessionType by
|
||||||
|
remember {
|
||||||
|
mutableStateOf(
|
||||||
|
SessionType.TRAINING
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
var sessionRevision by
|
var sessionRevision by
|
||||||
remember {
|
remember {
|
||||||
mutableIntStateOf(0)
|
mutableIntStateOf(0)
|
||||||
|
|
@ -93,9 +103,89 @@ fun SessionScreen(
|
||||||
accent = colors.muted,
|
accent = colors.muted,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
TrainlogFrame(
|
||||||
|
title = "TYPE DE SEANCE"
|
||||||
|
) {
|
||||||
|
TrainlogAction(
|
||||||
|
label =
|
||||||
|
if (
|
||||||
|
sessionType ==
|
||||||
|
SessionType.TRAINING
|
||||||
|
) {
|
||||||
|
"[✓] Entraînement"
|
||||||
|
} else {
|
||||||
|
"[ ] Entraînement"
|
||||||
|
},
|
||||||
|
description =
|
||||||
|
"Séance normale de travail.",
|
||||||
|
accent =
|
||||||
|
if (
|
||||||
|
sessionType ==
|
||||||
|
SessionType.TRAINING
|
||||||
|
) {
|
||||||
|
colors.success
|
||||||
|
} else {
|
||||||
|
colors.muted
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
sessionType =
|
||||||
|
SessionType.TRAINING
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
TrainlogAction(
|
||||||
|
label =
|
||||||
|
if (
|
||||||
|
sessionType ==
|
||||||
|
SessionType.MAX_TEST
|
||||||
|
) {
|
||||||
|
"[✓] Test max"
|
||||||
|
} else {
|
||||||
|
"[ ] Test max"
|
||||||
|
},
|
||||||
|
description =
|
||||||
|
"Séance explicitement dédiée à une mesure de max.",
|
||||||
|
accent =
|
||||||
|
if (
|
||||||
|
sessionType ==
|
||||||
|
SessionType.MAX_TEST
|
||||||
|
) {
|
||||||
|
colors.warning
|
||||||
|
} else {
|
||||||
|
colors.muted
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
sessionType =
|
||||||
|
SessionType.MAX_TEST
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
TrainlogFrame(
|
TrainlogFrame(
|
||||||
title = "SEANCE EN COURS"
|
title = "SEANCE EN COURS"
|
||||||
) {
|
) {
|
||||||
|
TrainlogInfo(
|
||||||
|
text =
|
||||||
|
"Type : " +
|
||||||
|
if (
|
||||||
|
sessionType ==
|
||||||
|
SessionType.MAX_TEST
|
||||||
|
) {
|
||||||
|
"TEST MAX"
|
||||||
|
} else {
|
||||||
|
"ENTRAÎNEMENT"
|
||||||
|
},
|
||||||
|
color =
|
||||||
|
if (
|
||||||
|
sessionType ==
|
||||||
|
SessionType.MAX_TEST
|
||||||
|
) {
|
||||||
|
colors.warning
|
||||||
|
} else {
|
||||||
|
colors.accent
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
draftExercises.isEmpty()
|
draftExercises.isEmpty()
|
||||||
) {
|
) {
|
||||||
|
|
@ -254,7 +344,9 @@ fun SessionScreen(
|
||||||
.saveSession(
|
.saveSession(
|
||||||
SessionDraft(
|
SessionDraft(
|
||||||
exercises =
|
exercises =
|
||||||
draftExercises
|
draftExercises,
|
||||||
|
sessionType =
|
||||||
|
sessionType,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
|
@ -265,6 +357,9 @@ fun SessionScreen(
|
||||||
selectedExercise =
|
selectedExercise =
|
||||||
null
|
null
|
||||||
|
|
||||||
|
sessionType =
|
||||||
|
SessionType.TRAINING
|
||||||
|
|
||||||
sessionRevision += 1
|
sessionRevision += 1
|
||||||
|
|
||||||
message =
|
message =
|
||||||
|
|
|
||||||
|
|
@ -242,3 +242,29 @@ Android is not intended to own:
|
||||||
- direct SQLite-file synchronization;
|
- direct SQLite-file synchronization;
|
||||||
- exercise-name heuristics;
|
- exercise-name heuristics;
|
||||||
- a mounted-filesystem dependency.
|
- a mounted-filesystem dependency.
|
||||||
|
|
||||||
|
## 15. Test-max sessions
|
||||||
|
|
||||||
|
Android session entry exposes:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Entraînement
|
||||||
|
Test max
|
||||||
|
```
|
||||||
|
|
||||||
|
The selection is persisted in the existing Android `sessions.session_type`
|
||||||
|
column and exported in the mobile snapshot as:
|
||||||
|
|
||||||
|
```text
|
||||||
|
training
|
||||||
|
max_test
|
||||||
|
```
|
||||||
|
|
||||||
|
History and detail visibly identify max-test sessions.
|
||||||
|
|
||||||
|
Selecting `Test max` is explicit metadata; Trainlog does not infer max tests
|
||||||
|
from large repetition or duration values.
|
||||||
|
|
||||||
|
Android's current session form still records the exercise data fields it
|
||||||
|
supports. Measured-max classification on the desktop uses only actual values
|
||||||
|
that were truly captured and synchronized.
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ ANDROID_SYNC_RECEIPT=PASS
|
||||||
TUI_SYNC_LOG_SHOW=PASS
|
TUI_SYNC_LOG_SHOW=PASS
|
||||||
BIDIRECTIONAL_SYNC_V1=PASS
|
BIDIRECTIONAL_SYNC_V1=PASS
|
||||||
|
|
||||||
DESKTOP_TESTS=19/19 PASS
|
DESKTOP_TESTS=20/20 PASS
|
||||||
ANDROID_BUILD=PASS
|
ANDROID_BUILD=PASS
|
||||||
HARDWARE_SYNC_VALIDATION=PASS
|
HARDWARE_SYNC_VALIDATION=PASS
|
||||||
```
|
```
|
||||||
|
|
@ -120,7 +120,7 @@ No mounted Android filesystem is required.
|
||||||
Desktop:
|
Desktop:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
19/19 Meson tests PASS
|
20/20 Meson tests PASS
|
||||||
frozen JSON validator PASS
|
frozen JSON validator PASS
|
||||||
import-contract validator PASS
|
import-contract validator PASS
|
||||||
git diff --check PASS
|
git diff --check PASS
|
||||||
|
|
@ -139,8 +139,31 @@ multiple distinct request IDs consumed once each PASS
|
||||||
No new feature is frozen by this documentation cleanup.
|
No new feature is frozen by this documentation cleanup.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
MEASURED_MAX_V1=PASS
|
||||||
|
WORKING_LOAD_PERCENTAGES=PASS
|
||||||
|
ANDROID_MAX_TEST_SESSION=PASS
|
||||||
NEXT_FEATURE=UNFROZEN
|
NEXT_FEATURE=UNFROZEN
|
||||||
```
|
```
|
||||||
|
|
||||||
Future work must start from this validated baseline rather than from obsolete
|
Future work must start from this validated baseline rather than from obsolete
|
||||||
historical `NEXT` notes.
|
historical `NEXT` notes.
|
||||||
|
|
||||||
|
## Measured max v1
|
||||||
|
|
||||||
|
```text
|
||||||
|
MEASURED_MAX_V1=PASS
|
||||||
|
MEASURED_MAX_ONLY_FROM_MAX_TEST=PASS
|
||||||
|
WORKING_LOAD_PERCENTAGES=PASS
|
||||||
|
ASSISTANCE_DIRECTION_AWARE=PASS
|
||||||
|
ANDROID_MAX_TEST_SESSION=PASS
|
||||||
|
DESKTOP_TESTS=20/20 PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
A measured maximum is derived only from explicit `max_test` sessions. Ordinary
|
||||||
|
training is never promoted implicitly.
|
||||||
|
|
||||||
|
The current measured result is the newest successful max test. The historical
|
||||||
|
record compares max tests using the same load mode.
|
||||||
|
|
||||||
|
External-load working percentages are pure calculations from the current
|
||||||
|
measured load; they are not persisted and no estimated 1RM is introduced.
|
||||||
|
|
|
||||||
|
|
@ -276,3 +276,38 @@ schema_v5_migration
|
||||||
```
|
```
|
||||||
|
|
||||||
The current normal suite contains 19 tests.
|
The current normal suite contains 19 tests.
|
||||||
|
|
||||||
|
## 11. Measured-max derivation
|
||||||
|
|
||||||
|
Measured maxima require no desktop schema v6.
|
||||||
|
|
||||||
|
The existing `sessions.session_type = max_test` classification plus actual
|
||||||
|
`performed_sets` are sufficient.
|
||||||
|
|
||||||
|
Exercise performance points carry the originating session type so the
|
||||||
|
measured-max layer can distinguish explicit tests from ordinary training.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
```text
|
||||||
|
training session
|
||||||
|
never becomes measured max implicitly
|
||||||
|
|
||||||
|
max_test + external
|
||||||
|
greatest successful actual load
|
||||||
|
tie -> greatest reps/duration
|
||||||
|
|
||||||
|
max_test + assistance
|
||||||
|
lowest successful assistance
|
||||||
|
tie -> greatest reps/duration
|
||||||
|
|
||||||
|
max_test + no load
|
||||||
|
greatest successful reps/duration
|
||||||
|
```
|
||||||
|
|
||||||
|
A zero-repetition failed attempt is not a successful measurement.
|
||||||
|
|
||||||
|
The current measured result is the newest successful max-test point. The record
|
||||||
|
is the best max-test point using the same load mode.
|
||||||
|
|
||||||
|
No extra maximum row is persisted; results are derived from canonical history.
|
||||||
|
|
|
||||||
|
|
@ -199,3 +199,29 @@ Continuous activity must never be:
|
||||||
- hidden in notes;
|
- hidden in notes;
|
||||||
- converted into a fake set;
|
- converted into a fake set;
|
||||||
- silently discarded.
|
- silently discarded.
|
||||||
|
|
||||||
|
## 10. Measured max semantics
|
||||||
|
|
||||||
|
`session_type = max_test` is an explicit semantic boundary.
|
||||||
|
|
||||||
|
Measured max v1 never equates an ordinary best set with a measured maximum.
|
||||||
|
|
||||||
|
For set-based exercises:
|
||||||
|
|
||||||
|
```text
|
||||||
|
external
|
||||||
|
max measured load = greatest successful actual load in a max_test
|
||||||
|
|
||||||
|
assistance
|
||||||
|
best measured assistance = lowest successful assistance in a max_test
|
||||||
|
|
||||||
|
none
|
||||||
|
measured max = greatest successful reps/duration in a max_test
|
||||||
|
```
|
||||||
|
|
||||||
|
Ties use greater repetitions/duration.
|
||||||
|
|
||||||
|
The newest successful explicit test is the current measured result. A separate
|
||||||
|
same-mode historical record may be older.
|
||||||
|
|
||||||
|
No estimated 1RM is mixed into this contract.
|
||||||
|
|
|
||||||
|
|
@ -161,3 +161,23 @@ fake performed sets for continuous activity
|
||||||
fake uniform targets for heterogeneous actual sets
|
fake uniform targets for heterogeneous actual sets
|
||||||
incompatible changes to Trainlog JSON v1
|
incompatible changes to Trainlog JSON v1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Measured max v1
|
||||||
|
|
||||||
|
```text
|
||||||
|
MEASURED_MAX_V1=PASS
|
||||||
|
MEASURED_MAX_ONLY_FROM_MAX_TEST=PASS
|
||||||
|
MEASURED_MAX_HISTORY=PASS
|
||||||
|
MEASURED_MAX_GRAPH=PASS
|
||||||
|
WORKING_LOAD_PERCENTAGES=PASS
|
||||||
|
ANDROID_MAX_TEST_SESSION=PASS
|
||||||
|
DESKTOP_TESTS=20/20 PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
No schema v6 and no Trainlog JSON v1 change were required.
|
||||||
|
|
||||||
|
The next product feature remains intentionally unfrozen:
|
||||||
|
|
||||||
|
```text
|
||||||
|
NEXT_FEATURE=UNFROZEN
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ Current normal suite:
|
||||||
Validated checkpoint:
|
Validated checkpoint:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
19/19 PASS
|
20/20 PASS
|
||||||
```
|
```
|
||||||
|
|
||||||
Notable regression coverage:
|
Notable regression coverage:
|
||||||
|
|
@ -214,3 +214,29 @@ JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./gradlew assembleDebug
|
||||||
|
|
||||||
Documentation must describe the resulting state, not retain contradictory old
|
Documentation must describe the resulting state, not retain contradictory old
|
||||||
`NEXT` checkpoints.
|
`NEXT` checkpoints.
|
||||||
|
|
||||||
|
## 11. Measured-max regression
|
||||||
|
|
||||||
|
The normal Meson suite contains:
|
||||||
|
|
||||||
|
```text
|
||||||
|
measured_max
|
||||||
|
```
|
||||||
|
|
||||||
|
Coverage proves:
|
||||||
|
|
||||||
|
- stronger ordinary training is ignored by measured-max classification;
|
||||||
|
- only `max_test` sessions participate;
|
||||||
|
- zero-repetition failed attempts are not promoted;
|
||||||
|
- newest successful explicit test is the current measurement;
|
||||||
|
- historical external-load record can remain older than current;
|
||||||
|
- lower assistance is better;
|
||||||
|
- no-load max tests compare actual reps/duration;
|
||||||
|
- external working loads round to the configured increment;
|
||||||
|
- working-load percentages reject assistance.
|
||||||
|
|
||||||
|
Current normal baseline:
|
||||||
|
|
||||||
|
```text
|
||||||
|
20/20 PASS
|
||||||
|
```
|
||||||
|
|
|
||||||
40
docs/tui.md
40
docs/tui.md
|
|
@ -264,3 +264,43 @@ Current normal suite:
|
||||||
```text
|
```text
|
||||||
19/19 PASS
|
19/19 PASS
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 15. Measured max view
|
||||||
|
|
||||||
|
From `3 Exercices`, the selected exercise exposes:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Enter ordinary performance history
|
||||||
|
m measured max
|
||||||
|
```
|
||||||
|
|
||||||
|
The measured-max page is deliberately separate from ordinary best-set history.
|
||||||
|
|
||||||
|
It shows:
|
||||||
|
|
||||||
|
- count of explicit max-test sessions;
|
||||||
|
- newest successful measured result;
|
||||||
|
- best historical result using the same load mode;
|
||||||
|
- dedicated max-test graph;
|
||||||
|
- max-test history;
|
||||||
|
- external-load working percentages at 60%, 70%, 80%, and 90%.
|
||||||
|
|
||||||
|
For external load, `r` cycles practical rounding increments:
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.5 kg
|
||||||
|
1.0 kg
|
||||||
|
2.5 kg
|
||||||
|
5.0 kg
|
||||||
|
```
|
||||||
|
|
||||||
|
Working percentages are display calculations only.
|
||||||
|
|
||||||
|
Assistance remains inverse-direction:
|
||||||
|
|
||||||
|
```text
|
||||||
|
less assistance = better
|
||||||
|
```
|
||||||
|
|
||||||
|
No percentage-of-max working load is produced for assistance or no-load
|
||||||
|
performance.
|
||||||
|
|
|
||||||
|
|
@ -197,6 +197,7 @@ TrainlogStatus trainlog_database_latest_body_pair(
|
||||||
typedef struct TrainlogExercisePerformancePoint {
|
typedef struct TrainlogExercisePerformancePoint {
|
||||||
char session_id[TRAINLOG_ID_MAX + 1U];
|
char session_id[TRAINLOG_ID_MAX + 1U];
|
||||||
char started_at[TRAINLOG_TIMESTAMP_MAX + 1U];
|
char started_at[TRAINLOG_TIMESTAMP_MAX + 1U];
|
||||||
|
TrainlogSessionType session_type;
|
||||||
TrainlogTrackingMode tracking_mode;
|
TrainlogTrackingMode tracking_mode;
|
||||||
TrainlogLoadMode load_mode;
|
TrainlogLoadMode load_mode;
|
||||||
size_t actual_set_count;
|
size_t actual_set_count;
|
||||||
|
|
|
||||||
60
tui/include/trainlog/measured_max.h
Normal file
60
tui/include/trainlog/measured_max.h
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
#ifndef TRAINLOG_MEASURED_MAX_H
|
||||||
|
#define TRAINLOG_MEASURED_MAX_H
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file measured_max.h
|
||||||
|
* @brief Explicit measured-max classification and working-load calculations.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#include "trainlog/database.h"
|
||||||
|
#include "trainlog/status.h"
|
||||||
|
|
||||||
|
typedef struct TrainlogMeasuredMaxSummary {
|
||||||
|
size_t test_count;
|
||||||
|
size_t successful_test_count;
|
||||||
|
bool found;
|
||||||
|
TrainlogExercisePerformancePoint current;
|
||||||
|
bool has_record;
|
||||||
|
TrainlogExercisePerformancePoint record;
|
||||||
|
} TrainlogMeasuredMaxSummary;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Summarize explicit max-test sessions from newest-first performance.
|
||||||
|
*
|
||||||
|
* Only points whose session_type is TRAINLOG_SESSION_MAX_TEST participate.
|
||||||
|
* Ordinary training performance is never promoted to a measured maximum.
|
||||||
|
*
|
||||||
|
* current is the newest successful explicit max-test result.
|
||||||
|
* record is the best successful max-test result using the same load mode as
|
||||||
|
* current:
|
||||||
|
*
|
||||||
|
* - external: greatest actual load, tie -> greatest reps/duration;
|
||||||
|
* - assistance: lowest assistance, tie -> greatest reps/duration;
|
||||||
|
* - no load: greatest reps/duration.
|
||||||
|
*
|
||||||
|
* No estimated 1RM is calculated.
|
||||||
|
*/
|
||||||
|
TrainlogStatus trainlog_measured_max_summarize(
|
||||||
|
const TrainlogExercisePerformancePoint *points,
|
||||||
|
size_t count,
|
||||||
|
TrainlogMeasuredMaxSummary *output
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Calculate a working load from the current measured external load.
|
||||||
|
*
|
||||||
|
* The result is percentage of the measured load rounded to the nearest
|
||||||
|
* positive increment. This calculation is deliberately unavailable for
|
||||||
|
* assistance and no-load performance.
|
||||||
|
*/
|
||||||
|
TrainlogStatus trainlog_measured_max_working_load(
|
||||||
|
const TrainlogExercisePerformancePoint *measured,
|
||||||
|
double percentage,
|
||||||
|
double increment_kg,
|
||||||
|
double *output_kg
|
||||||
|
);
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -36,6 +36,7 @@ trainlog_core_sources = files(
|
||||||
'src/usb.c',
|
'src/usb.c',
|
||||||
'src/mtp.c',
|
'src/mtp.c',
|
||||||
'src/reps.c',
|
'src/reps.c',
|
||||||
|
'src/measured_max.c',
|
||||||
'src/sync.c',
|
'src/sync.c',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -341,3 +342,15 @@ trainlog_sync_once = executable(
|
||||||
dependencies: trainlog_core_dep,
|
dependencies: trainlog_core_dep,
|
||||||
c_args: strict_c_args,
|
c_args: strict_c_args,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
test_measured_max = executable(
|
||||||
|
'test_measured_max',
|
||||||
|
'tests/test_measured_max.c',
|
||||||
|
dependencies: trainlog_core_dep,
|
||||||
|
c_args: strict_c_args,
|
||||||
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
'measured_max',
|
||||||
|
test_measured_max,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -3340,6 +3340,7 @@ static bool performance_candidate_better(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* TRAINLOG_MEASURED_MAX_SESSION_TYPE_V1 */
|
||||||
TrainlogStatus trainlog_database_list_exercise_performance(
|
TrainlogStatus trainlog_database_list_exercise_performance(
|
||||||
TrainlogDatabase *database,
|
TrainlogDatabase *database,
|
||||||
const char *exercise_id,
|
const char *exercise_id,
|
||||||
|
|
@ -3352,6 +3353,7 @@ TrainlogStatus trainlog_database_list_exercise_performance(
|
||||||
"SELECT "
|
"SELECT "
|
||||||
"s.session_id, "
|
"s.session_id, "
|
||||||
"s.started_at, "
|
"s.started_at, "
|
||||||
|
"s.session_type, "
|
||||||
"e.tracking_mode, "
|
"e.tracking_mode, "
|
||||||
"se.load_mode, "
|
"se.load_mode, "
|
||||||
"ps.id, "
|
"ps.id, "
|
||||||
|
|
@ -3377,13 +3379,19 @@ TrainlogStatus trainlog_database_list_exercise_performance(
|
||||||
size_t copied = 0U;
|
size_t copied = 0U;
|
||||||
int rc;
|
int rc;
|
||||||
|
|
||||||
if (database == NULL ||
|
if (
|
||||||
|
database == NULL ||
|
||||||
database->connection == NULL ||
|
database->connection == NULL ||
|
||||||
exercise_id == NULL ||
|
exercise_id == NULL ||
|
||||||
exercise_id[0] == '\0' ||
|
exercise_id[0] == '\0' ||
|
||||||
output_count == NULL ||
|
output_count == NULL ||
|
||||||
(capacity > 0U && output == NULL)) {
|
(
|
||||||
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
capacity > 0U &&
|
||||||
|
output == NULL
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
TRAINLOG_STATUS_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
*output_count = 0U;
|
*output_count = 0U;
|
||||||
|
|
@ -3398,7 +3406,8 @@ TrainlogStatus trainlog_database_list_exercise_performance(
|
||||||
);
|
);
|
||||||
|
|
||||||
if (rc != SQLITE_OK) {
|
if (rc != SQLITE_OK) {
|
||||||
return TRAINLOG_STATUS_DATABASE_ERROR;
|
return
|
||||||
|
TRAINLOG_STATUS_DATABASE_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
rc = sqlite3_bind_text(
|
rc = sqlite3_bind_text(
|
||||||
|
|
@ -3410,31 +3419,63 @@ TrainlogStatus trainlog_database_list_exercise_performance(
|
||||||
);
|
);
|
||||||
|
|
||||||
if (rc != SQLITE_OK) {
|
if (rc != SQLITE_OK) {
|
||||||
(void)sqlite3_finalize(statement);
|
(void)sqlite3_finalize(
|
||||||
return TRAINLOG_STATUS_DATABASE_ERROR;
|
statement
|
||||||
|
);
|
||||||
|
|
||||||
|
return
|
||||||
|
TRAINLOG_STATUS_DATABASE_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
while ((rc = sqlite3_step(statement)) == SQLITE_ROW) {
|
while (
|
||||||
|
(rc = sqlite3_step(statement)) ==
|
||||||
|
SQLITE_ROW
|
||||||
|
) {
|
||||||
const unsigned char *session_id =
|
const unsigned char *session_id =
|
||||||
sqlite3_column_text(statement, 0);
|
sqlite3_column_text(
|
||||||
|
statement,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
const unsigned char *started_at =
|
const unsigned char *started_at =
|
||||||
sqlite3_column_text(statement, 1);
|
sqlite3_column_text(
|
||||||
|
statement,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
const unsigned char *session_type =
|
||||||
|
sqlite3_column_text(
|
||||||
|
statement,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
|
||||||
const unsigned char *tracking_mode =
|
const unsigned char *tracking_mode =
|
||||||
sqlite3_column_text(statement, 2);
|
sqlite3_column_text(
|
||||||
|
statement,
|
||||||
|
3
|
||||||
|
);
|
||||||
|
|
||||||
const unsigned char *load_mode =
|
const unsigned char *load_mode =
|
||||||
sqlite3_column_text(statement, 3);
|
sqlite3_column_text(
|
||||||
|
statement,
|
||||||
|
4
|
||||||
|
);
|
||||||
|
|
||||||
bool new_session;
|
bool new_session;
|
||||||
|
|
||||||
if (session_id == NULL ||
|
if (
|
||||||
|
session_id == NULL ||
|
||||||
started_at == NULL ||
|
started_at == NULL ||
|
||||||
|
session_type == NULL ||
|
||||||
tracking_mode == NULL ||
|
tracking_mode == NULL ||
|
||||||
load_mode == NULL) {
|
load_mode == NULL
|
||||||
(void)sqlite3_finalize(statement);
|
) {
|
||||||
return TRAINLOG_STATUS_DATABASE_ERROR;
|
(void)sqlite3_finalize(
|
||||||
|
statement
|
||||||
|
);
|
||||||
|
|
||||||
|
return
|
||||||
|
TRAINLOG_STATUS_DATABASE_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
new_session =
|
new_session =
|
||||||
|
|
@ -3455,7 +3496,8 @@ TrainlogStatus trainlog_database_list_exercise_performance(
|
||||||
current = NULL;
|
current = NULL;
|
||||||
|
|
||||||
if (copied < capacity) {
|
if (copied < capacity) {
|
||||||
current = &output[copied];
|
current =
|
||||||
|
&output[copied];
|
||||||
|
|
||||||
(void)memset(
|
(void)memset(
|
||||||
current,
|
current,
|
||||||
|
|
@ -3477,6 +3519,20 @@ TrainlogStatus trainlog_database_list_exercise_performance(
|
||||||
(const char *)started_at
|
(const char *)started_at
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!session_type_from_sql(
|
||||||
|
(const char *)session_type,
|
||||||
|
¤t->session_type
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
(void)sqlite3_finalize(
|
||||||
|
statement
|
||||||
|
);
|
||||||
|
|
||||||
|
return
|
||||||
|
TRAINLOG_STATUS_DATABASE_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
current->tracking_mode =
|
current->tracking_mode =
|
||||||
tracking_mode_from_sql(
|
tracking_mode_from_sql(
|
||||||
(const char *)tracking_mode
|
(const char *)tracking_mode
|
||||||
|
|
@ -3491,63 +3547,100 @@ TrainlogStatus trainlog_database_list_exercise_performance(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current != NULL &&
|
if (
|
||||||
sqlite3_column_type(statement, 4) != SQLITE_NULL) {
|
current != NULL &&
|
||||||
|
sqlite3_column_type(
|
||||||
|
statement,
|
||||||
|
5
|
||||||
|
) != SQLITE_NULL
|
||||||
|
) {
|
||||||
int metric_value;
|
int metric_value;
|
||||||
int has_weight;
|
int has_weight;
|
||||||
double weight_kg;
|
double weight_kg;
|
||||||
|
|
||||||
++current->actual_set_count;
|
++current->actual_set_count;
|
||||||
|
|
||||||
if (current->tracking_mode ==
|
if (
|
||||||
TRAINLOG_TRACKING_REPS) {
|
current->tracking_mode ==
|
||||||
|
TRAINLOG_TRACKING_REPS
|
||||||
|
) {
|
||||||
metric_value =
|
metric_value =
|
||||||
sqlite3_column_type(statement, 5) !=
|
sqlite3_column_type(
|
||||||
SQLITE_NULL
|
statement,
|
||||||
? sqlite3_column_int(statement, 5)
|
6
|
||||||
|
) != SQLITE_NULL
|
||||||
|
? sqlite3_column_int(
|
||||||
|
statement,
|
||||||
|
6
|
||||||
|
)
|
||||||
: 0;
|
: 0;
|
||||||
} else {
|
} else {
|
||||||
metric_value =
|
metric_value =
|
||||||
sqlite3_column_type(statement, 6) !=
|
sqlite3_column_type(
|
||||||
SQLITE_NULL
|
statement,
|
||||||
? sqlite3_column_int(statement, 6)
|
7
|
||||||
|
) != SQLITE_NULL
|
||||||
|
? sqlite3_column_int(
|
||||||
|
statement,
|
||||||
|
7
|
||||||
|
)
|
||||||
: 0;
|
: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
has_weight =
|
has_weight =
|
||||||
sqlite3_column_type(statement, 7) !=
|
sqlite3_column_type(
|
||||||
SQLITE_NULL;
|
statement,
|
||||||
|
8
|
||||||
|
) != SQLITE_NULL;
|
||||||
|
|
||||||
weight_kg =
|
weight_kg =
|
||||||
has_weight != 0
|
has_weight != 0
|
||||||
? sqlite3_column_double(statement, 7)
|
? sqlite3_column_double(
|
||||||
|
statement,
|
||||||
|
8
|
||||||
|
)
|
||||||
: 0.0;
|
: 0.0;
|
||||||
|
|
||||||
if (performance_candidate_better(
|
if (
|
||||||
|
performance_candidate_better(
|
||||||
current,
|
current,
|
||||||
current->load_mode,
|
current->load_mode,
|
||||||
metric_value,
|
metric_value,
|
||||||
has_weight,
|
has_weight,
|
||||||
weight_kg
|
weight_kg
|
||||||
)) {
|
)
|
||||||
|
) {
|
||||||
current->has_performance = 1;
|
current->has_performance = 1;
|
||||||
current->metric_value = metric_value;
|
current->metric_value =
|
||||||
current->has_weight = has_weight;
|
metric_value;
|
||||||
current->weight_kg = weight_kg;
|
current->has_weight =
|
||||||
|
has_weight;
|
||||||
|
current->weight_kg =
|
||||||
|
weight_kg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rc != SQLITE_DONE) {
|
if (rc != SQLITE_DONE) {
|
||||||
(void)sqlite3_finalize(statement);
|
(void)sqlite3_finalize(
|
||||||
return TRAINLOG_STATUS_DATABASE_ERROR;
|
statement
|
||||||
|
);
|
||||||
|
|
||||||
|
return
|
||||||
|
TRAINLOG_STATUS_DATABASE_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sqlite3_finalize(statement) != SQLITE_OK) {
|
if (
|
||||||
return TRAINLOG_STATUS_DATABASE_ERROR;
|
sqlite3_finalize(
|
||||||
|
statement
|
||||||
|
) != SQLITE_OK
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
TRAINLOG_STATUS_DATABASE_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
*output_count = copied;
|
*output_count = copied;
|
||||||
|
|
||||||
return TRAINLOG_STATUS_OK;
|
return TRAINLOG_STATUS_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
205
tui/src/measured_max.c
Normal file
205
tui/src/measured_max.c
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
/**
|
||||||
|
* @file measured_max.c
|
||||||
|
* @brief Explicit measured-max and working-load implementation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "trainlog/measured_max.h"
|
||||||
|
|
||||||
|
#include <math.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static bool measured_point_better(
|
||||||
|
const TrainlogExercisePerformancePoint *candidate,
|
||||||
|
const TrainlogExercisePerformancePoint *current
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
candidate == NULL ||
|
||||||
|
candidate->has_performance == 0
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
current == NULL ||
|
||||||
|
current->has_performance == 0
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
candidate->load_mode !=
|
||||||
|
current->load_mode
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (candidate->load_mode) {
|
||||||
|
case TRAINLOG_LOAD_EXTERNAL:
|
||||||
|
if (candidate->has_weight == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
candidate->weight_kg >
|
||||||
|
current->weight_kg
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
candidate->weight_kg ==
|
||||||
|
current->weight_kg &&
|
||||||
|
candidate->metric_value >
|
||||||
|
current->metric_value;
|
||||||
|
|
||||||
|
case TRAINLOG_LOAD_ASSISTANCE:
|
||||||
|
if (candidate->has_weight == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
candidate->weight_kg <
|
||||||
|
current->weight_kg
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
candidate->weight_kg ==
|
||||||
|
current->weight_kg &&
|
||||||
|
candidate->metric_value >
|
||||||
|
current->metric_value;
|
||||||
|
|
||||||
|
case TRAINLOG_LOAD_NONE:
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
candidate->metric_value >
|
||||||
|
current->metric_value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TrainlogStatus trainlog_measured_max_summarize(
|
||||||
|
const TrainlogExercisePerformancePoint *points,
|
||||||
|
size_t count,
|
||||||
|
TrainlogMeasuredMaxSummary *output
|
||||||
|
)
|
||||||
|
{
|
||||||
|
size_t index;
|
||||||
|
|
||||||
|
if (
|
||||||
|
output == NULL ||
|
||||||
|
(
|
||||||
|
count > 0U &&
|
||||||
|
points == NULL
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
TRAINLOG_STATUS_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
(void)memset(
|
||||||
|
output,
|
||||||
|
0,
|
||||||
|
sizeof(*output)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (
|
||||||
|
index = 0U;
|
||||||
|
index < count;
|
||||||
|
++index
|
||||||
|
) {
|
||||||
|
const TrainlogExercisePerformancePoint *point =
|
||||||
|
&points[index];
|
||||||
|
|
||||||
|
if (
|
||||||
|
point->session_type !=
|
||||||
|
TRAINLOG_SESSION_MAX_TEST
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
++output->test_count;
|
||||||
|
|
||||||
|
if (point->has_performance == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
++output->successful_test_count;
|
||||||
|
|
||||||
|
if (!output->found) {
|
||||||
|
output->found = true;
|
||||||
|
output->current = *point;
|
||||||
|
output->has_record = true;
|
||||||
|
output->record = *point;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
point->load_mode ==
|
||||||
|
output->current.load_mode &&
|
||||||
|
measured_point_better(
|
||||||
|
point,
|
||||||
|
&output->record
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
output->record = *point;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return TRAINLOG_STATUS_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
TrainlogStatus trainlog_measured_max_working_load(
|
||||||
|
const TrainlogExercisePerformancePoint *measured,
|
||||||
|
double percentage,
|
||||||
|
double increment_kg,
|
||||||
|
double *output_kg
|
||||||
|
)
|
||||||
|
{
|
||||||
|
double raw;
|
||||||
|
double rounded;
|
||||||
|
|
||||||
|
if (
|
||||||
|
measured == NULL ||
|
||||||
|
output_kg == NULL ||
|
||||||
|
measured->session_type !=
|
||||||
|
TRAINLOG_SESSION_MAX_TEST ||
|
||||||
|
measured->has_performance == 0 ||
|
||||||
|
measured->load_mode !=
|
||||||
|
TRAINLOG_LOAD_EXTERNAL ||
|
||||||
|
measured->has_weight == 0 ||
|
||||||
|
!isfinite(measured->weight_kg) ||
|
||||||
|
measured->weight_kg <= 0.0 ||
|
||||||
|
!isfinite(percentage) ||
|
||||||
|
percentage <= 0.0 ||
|
||||||
|
percentage > 100.0 ||
|
||||||
|
!isfinite(increment_kg) ||
|
||||||
|
increment_kg <= 0.0
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
TRAINLOG_STATUS_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
raw =
|
||||||
|
measured->weight_kg *
|
||||||
|
percentage /
|
||||||
|
100.0;
|
||||||
|
|
||||||
|
rounded =
|
||||||
|
floor(
|
||||||
|
raw /
|
||||||
|
increment_kg +
|
||||||
|
0.5
|
||||||
|
) *
|
||||||
|
increment_kg;
|
||||||
|
|
||||||
|
if (rounded <= 0.0) {
|
||||||
|
rounded =
|
||||||
|
increment_kg;
|
||||||
|
}
|
||||||
|
|
||||||
|
*output_kg = rounded;
|
||||||
|
|
||||||
|
return TRAINLOG_STATUS_OK;
|
||||||
|
}
|
||||||
529
tui/src/tui.c
529
tui/src/tui.c
|
|
@ -24,6 +24,7 @@
|
||||||
#include "trainlog/catalog.h"
|
#include "trainlog/catalog.h"
|
||||||
#include "trainlog/duration.h"
|
#include "trainlog/duration.h"
|
||||||
#include "trainlog/id.h"
|
#include "trainlog/id.h"
|
||||||
|
#include "trainlog/measured_max.h"
|
||||||
#include "trainlog/mtp.h"
|
#include "trainlog/mtp.h"
|
||||||
#include "trainlog/sync.h"
|
#include "trainlog/sync.h"
|
||||||
#include "trainlog/reps.h"
|
#include "trainlog/reps.h"
|
||||||
|
|
@ -642,7 +643,8 @@ static void draw_exercise_performance_graph(
|
||||||
TrainlogLoadMode mode,
|
TrainlogLoadMode mode,
|
||||||
TrainlogTrackingMode tracking_mode,
|
TrainlogTrackingMode tracking_mode,
|
||||||
int top,
|
int top,
|
||||||
int height
|
int height,
|
||||||
|
bool measured_max
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
size_t indices[EXERCISE_GRAPH_POINTS];
|
size_t indices[EXERCISE_GRAPH_POINTS];
|
||||||
|
|
@ -720,7 +722,9 @@ static void draw_exercise_performance_graph(
|
||||||
mvprintw(
|
mvprintw(
|
||||||
top,
|
top,
|
||||||
left,
|
left,
|
||||||
"Assistance (kg) — moins = mieux"
|
measured_max
|
||||||
|
? "Assistance mesurée (kg) — moins = mieux"
|
||||||
|
: "Assistance (kg) — moins = mieux"
|
||||||
);
|
);
|
||||||
|
|
||||||
attroff(
|
attroff(
|
||||||
|
|
@ -733,7 +737,9 @@ static void draw_exercise_performance_graph(
|
||||||
mvprintw(
|
mvprintw(
|
||||||
top,
|
top,
|
||||||
left,
|
left,
|
||||||
"Charge du meilleur set (kg)"
|
measured_max
|
||||||
|
? "Max mesuré (kg)"
|
||||||
|
: "Charge du meilleur set (kg)"
|
||||||
);
|
);
|
||||||
} else if (
|
} else if (
|
||||||
tracking_mode ==
|
tracking_mode ==
|
||||||
|
|
@ -742,13 +748,17 @@ static void draw_exercise_performance_graph(
|
||||||
mvprintw(
|
mvprintw(
|
||||||
top,
|
top,
|
||||||
left,
|
left,
|
||||||
"Meilleure durée"
|
measured_max
|
||||||
|
? "Durée max mesurée"
|
||||||
|
: "Meilleure durée"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
mvprintw(
|
mvprintw(
|
||||||
top,
|
top,
|
||||||
left,
|
left,
|
||||||
"Meilleures répétitions"
|
measured_max
|
||||||
|
? "Répétitions max mesurées"
|
||||||
|
: "Meilleures répétitions"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1234,7 +1244,8 @@ static void screen_exercise_performance(
|
||||||
graph_mode,
|
graph_mode,
|
||||||
exercise->tracking_mode,
|
exercise->tracking_mode,
|
||||||
graph_content_top,
|
graph_content_top,
|
||||||
graph_height
|
graph_height,
|
||||||
|
false
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1300,6 +1311,497 @@ static void screen_exercise_performance(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* TRAINLOG_MEASURED_MAX_TUI_V1 */
|
||||||
|
|
||||||
|
static void screen_exercise_measured_max(
|
||||||
|
TrainlogDatabase *database,
|
||||||
|
const TrainlogExercise *exercise
|
||||||
|
)
|
||||||
|
{
|
||||||
|
static const double increments[] = {
|
||||||
|
0.5,
|
||||||
|
1.0,
|
||||||
|
2.5,
|
||||||
|
5.0
|
||||||
|
};
|
||||||
|
|
||||||
|
static const double percentages[] = {
|
||||||
|
60.0,
|
||||||
|
70.0,
|
||||||
|
80.0,
|
||||||
|
90.0
|
||||||
|
};
|
||||||
|
|
||||||
|
size_t increment_index = 2U;
|
||||||
|
|
||||||
|
if (
|
||||||
|
database == NULL ||
|
||||||
|
exercise == NULL
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
TrainlogExercisePerformancePoint
|
||||||
|
points[MAX_SESSIONS];
|
||||||
|
|
||||||
|
TrainlogExercisePerformancePoint
|
||||||
|
max_points[MAX_SESSIONS];
|
||||||
|
|
||||||
|
TrainlogMeasuredMaxSummary summary;
|
||||||
|
|
||||||
|
size_t count = 0U;
|
||||||
|
size_t max_count = 0U;
|
||||||
|
size_t index;
|
||||||
|
size_t history_limit;
|
||||||
|
|
||||||
|
bool decorated =
|
||||||
|
COLS >= 100 &&
|
||||||
|
LINES >= 30;
|
||||||
|
|
||||||
|
int summary_top =
|
||||||
|
decorated ? 8 : 3;
|
||||||
|
|
||||||
|
int summary_bottom =
|
||||||
|
decorated ? 15 : 9;
|
||||||
|
|
||||||
|
int graph_top =
|
||||||
|
decorated ? 16 : 10;
|
||||||
|
|
||||||
|
int graph_bottom =
|
||||||
|
decorated ? 23 : 17;
|
||||||
|
|
||||||
|
int history_top =
|
||||||
|
decorated ? 24 : 18;
|
||||||
|
|
||||||
|
int history_bottom =
|
||||||
|
LINES - 4;
|
||||||
|
|
||||||
|
int key;
|
||||||
|
|
||||||
|
if (
|
||||||
|
trainlog_database_list_exercise_performance(
|
||||||
|
database,
|
||||||
|
exercise->exercise_id,
|
||||||
|
points,
|
||||||
|
MAX_SESSIONS,
|
||||||
|
&count
|
||||||
|
) != TRAINLOG_STATUS_OK ||
|
||||||
|
trainlog_measured_max_summarize(
|
||||||
|
points,
|
||||||
|
count,
|
||||||
|
&summary
|
||||||
|
) != TRAINLOG_STATUS_OK
|
||||||
|
) {
|
||||||
|
draw_shell(
|
||||||
|
"TRAINLOG — Max mesuré",
|
||||||
|
"Une touche pour revenir"
|
||||||
|
);
|
||||||
|
|
||||||
|
status_line(
|
||||||
|
"Impossible de lire les tests de max.",
|
||||||
|
TRAINLOG_COLOR_ERROR
|
||||||
|
);
|
||||||
|
|
||||||
|
wait_key();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (
|
||||||
|
index = 0U;
|
||||||
|
index < count &&
|
||||||
|
max_count < MAX_SESSIONS;
|
||||||
|
++index
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
points[index].session_type ==
|
||||||
|
TRAINLOG_SESSION_MAX_TEST
|
||||||
|
) {
|
||||||
|
max_points[max_count] =
|
||||||
|
points[index];
|
||||||
|
|
||||||
|
++max_count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
erase();
|
||||||
|
box(
|
||||||
|
stdscr,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (decorated) {
|
||||||
|
section_ascii_header(
|
||||||
|
":: M A X M E S U R E ::"
|
||||||
|
);
|
||||||
|
|
||||||
|
dashboard_panel(
|
||||||
|
summary_top,
|
||||||
|
2,
|
||||||
|
summary_bottom,
|
||||||
|
COLS - 3,
|
||||||
|
"MAX MESURE"
|
||||||
|
);
|
||||||
|
|
||||||
|
dashboard_panel(
|
||||||
|
graph_top,
|
||||||
|
2,
|
||||||
|
graph_bottom,
|
||||||
|
COLS - 3,
|
||||||
|
"EVOLUTION DES TESTS MAX"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
history_bottom >
|
||||||
|
history_top + 1
|
||||||
|
) {
|
||||||
|
exercise_panel(
|
||||||
|
history_top,
|
||||||
|
2,
|
||||||
|
history_bottom,
|
||||||
|
COLS - 3,
|
||||||
|
"HISTORIQUE TESTS MAX"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
attron(
|
||||||
|
trainlog_theme_attribute(
|
||||||
|
TRAINLOG_COLOR_MUTED
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
mvprintw(
|
||||||
|
LINES - 2,
|
||||||
|
2,
|
||||||
|
"%.*s",
|
||||||
|
COLS - 4,
|
||||||
|
"r arrondi charge b/Échap retour"
|
||||||
|
);
|
||||||
|
|
||||||
|
attroff(
|
||||||
|
trainlog_theme_attribute(
|
||||||
|
TRAINLOG_COLOR_MUTED
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
draw_shell(
|
||||||
|
"TRAINLOG — Max mesuré",
|
||||||
|
"r arrondi charge b/Échap retour"
|
||||||
|
);
|
||||||
|
|
||||||
|
exercise_panel(
|
||||||
|
summary_top,
|
||||||
|
2,
|
||||||
|
summary_bottom,
|
||||||
|
COLS - 3,
|
||||||
|
"MAX MESURE"
|
||||||
|
);
|
||||||
|
|
||||||
|
exercise_panel(
|
||||||
|
graph_top,
|
||||||
|
2,
|
||||||
|
graph_bottom,
|
||||||
|
COLS - 3,
|
||||||
|
"EVOLUTION"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
history_bottom >
|
||||||
|
history_top + 1
|
||||||
|
) {
|
||||||
|
exercise_panel(
|
||||||
|
history_top,
|
||||||
|
2,
|
||||||
|
history_bottom,
|
||||||
|
COLS - 3,
|
||||||
|
"HISTORIQUE"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attron(
|
||||||
|
A_BOLD |
|
||||||
|
trainlog_theme_attribute(
|
||||||
|
TRAINLOG_COLOR_ACCENT
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
mvprintw(
|
||||||
|
summary_top + 1,
|
||||||
|
5,
|
||||||
|
"%s",
|
||||||
|
exercise->name
|
||||||
|
);
|
||||||
|
|
||||||
|
attroff(
|
||||||
|
A_BOLD |
|
||||||
|
trainlog_theme_attribute(
|
||||||
|
TRAINLOG_COLOR_ACCENT
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
mvprintw(
|
||||||
|
summary_top + 2,
|
||||||
|
5,
|
||||||
|
"Tests max : %zu · réussis : %zu",
|
||||||
|
summary.test_count,
|
||||||
|
summary.successful_test_count
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!summary.found) {
|
||||||
|
mvprintw(
|
||||||
|
summary_top + 3,
|
||||||
|
5,
|
||||||
|
"Aucun max mesuré réussi."
|
||||||
|
);
|
||||||
|
|
||||||
|
mvprintw(
|
||||||
|
summary_top + 4,
|
||||||
|
5,
|
||||||
|
"Seules les séances explicitement « Test de max » comptent."
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
char current_text[128];
|
||||||
|
char record_text[128];
|
||||||
|
char current_date[11];
|
||||||
|
char record_date[11];
|
||||||
|
|
||||||
|
exercise_format_performance(
|
||||||
|
&summary.current,
|
||||||
|
current_text,
|
||||||
|
sizeof(current_text)
|
||||||
|
);
|
||||||
|
|
||||||
|
exercise_format_performance(
|
||||||
|
&summary.record,
|
||||||
|
record_text,
|
||||||
|
sizeof(record_text)
|
||||||
|
);
|
||||||
|
|
||||||
|
exercise_short_date(
|
||||||
|
summary.current.started_at,
|
||||||
|
current_date
|
||||||
|
);
|
||||||
|
|
||||||
|
exercise_short_date(
|
||||||
|
summary.record.started_at,
|
||||||
|
record_date
|
||||||
|
);
|
||||||
|
|
||||||
|
mvprintw(
|
||||||
|
summary_top + 3,
|
||||||
|
5,
|
||||||
|
"Actuel : %s · %.*s",
|
||||||
|
current_date,
|
||||||
|
COLS - 32,
|
||||||
|
current_text
|
||||||
|
);
|
||||||
|
|
||||||
|
mvprintw(
|
||||||
|
summary_top + 4,
|
||||||
|
5,
|
||||||
|
"Record même mode : %s · %.*s",
|
||||||
|
record_date,
|
||||||
|
COLS - 42,
|
||||||
|
record_text
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
summary.current.load_mode ==
|
||||||
|
TRAINLOG_LOAD_EXTERNAL
|
||||||
|
) {
|
||||||
|
double working[4];
|
||||||
|
bool valid = true;
|
||||||
|
|
||||||
|
for (
|
||||||
|
index = 0U;
|
||||||
|
index < 4U;
|
||||||
|
++index
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
trainlog_measured_max_working_load(
|
||||||
|
&summary.current,
|
||||||
|
percentages[index],
|
||||||
|
increments[increment_index],
|
||||||
|
&working[index]
|
||||||
|
) != TRAINLOG_STATUS_OK
|
||||||
|
) {
|
||||||
|
valid = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valid) {
|
||||||
|
mvprintw(
|
||||||
|
summary_top + 5,
|
||||||
|
5,
|
||||||
|
"Travail : 60%% %.1f · 70%% %.1f · 80%% %.1f · 90%% %.1f kg",
|
||||||
|
working[0],
|
||||||
|
working[1],
|
||||||
|
working[2],
|
||||||
|
working[3]
|
||||||
|
);
|
||||||
|
|
||||||
|
mvprintw(
|
||||||
|
summary_top + 6,
|
||||||
|
5,
|
||||||
|
"Arrondi : %.1f kg (r pour changer) · aucun 1RM estimé",
|
||||||
|
increments[increment_index]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
summary.current.load_mode ==
|
||||||
|
TRAINLOG_LOAD_ASSISTANCE
|
||||||
|
) {
|
||||||
|
attron(
|
||||||
|
trainlog_theme_attribute(
|
||||||
|
TRAINLOG_COLOR_WARNING
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
mvprintw(
|
||||||
|
summary_top + 5,
|
||||||
|
5,
|
||||||
|
"Assistance : moins de kg = mieux."
|
||||||
|
);
|
||||||
|
|
||||||
|
mvprintw(
|
||||||
|
summary_top + 6,
|
||||||
|
5,
|
||||||
|
"Pourcentages de charge non applicables à l'assistance."
|
||||||
|
);
|
||||||
|
|
||||||
|
attroff(
|
||||||
|
trainlog_theme_attribute(
|
||||||
|
TRAINLOG_COLOR_WARNING
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
mvprintw(
|
||||||
|
summary_top + 5,
|
||||||
|
5,
|
||||||
|
"Sans charge externe : pourcentages non applicables."
|
||||||
|
);
|
||||||
|
|
||||||
|
mvprintw(
|
||||||
|
summary_top + 6,
|
||||||
|
5,
|
||||||
|
"Le max reste une valeur réellement réalisée, jamais estimée."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
summary.found &&
|
||||||
|
max_count > 0U
|
||||||
|
) {
|
||||||
|
int graph_content_top =
|
||||||
|
graph_top + 1;
|
||||||
|
|
||||||
|
int graph_height =
|
||||||
|
graph_bottom -
|
||||||
|
graph_top -
|
||||||
|
2;
|
||||||
|
|
||||||
|
draw_exercise_performance_graph(
|
||||||
|
max_points,
|
||||||
|
max_count,
|
||||||
|
summary.current.load_mode,
|
||||||
|
exercise->tracking_mode,
|
||||||
|
graph_content_top,
|
||||||
|
graph_height,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
mvprintw(
|
||||||
|
graph_top + 2,
|
||||||
|
5,
|
||||||
|
"Aucun point de max mesuré à tracer."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
history_bottom >
|
||||||
|
history_top + 1
|
||||||
|
) {
|
||||||
|
int first_row =
|
||||||
|
history_top + 1;
|
||||||
|
|
||||||
|
history_limit =
|
||||||
|
history_bottom >
|
||||||
|
first_row
|
||||||
|
? (size_t)(
|
||||||
|
history_bottom -
|
||||||
|
first_row
|
||||||
|
)
|
||||||
|
: 0U;
|
||||||
|
|
||||||
|
for (
|
||||||
|
index = 0U;
|
||||||
|
index < max_count &&
|
||||||
|
index < history_limit;
|
||||||
|
++index
|
||||||
|
) {
|
||||||
|
char date[11];
|
||||||
|
char text[128];
|
||||||
|
|
||||||
|
exercise_short_date(
|
||||||
|
max_points[index].started_at,
|
||||||
|
date
|
||||||
|
);
|
||||||
|
|
||||||
|
exercise_format_performance(
|
||||||
|
&max_points[index],
|
||||||
|
text,
|
||||||
|
sizeof(text)
|
||||||
|
);
|
||||||
|
|
||||||
|
mvprintw(
|
||||||
|
first_row +
|
||||||
|
(int)index,
|
||||||
|
5,
|
||||||
|
"%s %-13s %.*s",
|
||||||
|
date,
|
||||||
|
exercise_load_mode_label(
|
||||||
|
max_points[index]
|
||||||
|
.load_mode
|
||||||
|
),
|
||||||
|
COLS - 40,
|
||||||
|
text
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
key = getch();
|
||||||
|
|
||||||
|
if (
|
||||||
|
key == 'b' ||
|
||||||
|
key == 'B' ||
|
||||||
|
key == 27 ||
|
||||||
|
key == '\n' ||
|
||||||
|
key == KEY_ENTER
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
key == 'r' ||
|
||||||
|
key == 'R'
|
||||||
|
) {
|
||||||
|
increment_index =
|
||||||
|
(
|
||||||
|
increment_index + 1U
|
||||||
|
) %
|
||||||
|
(
|
||||||
|
sizeof(increments) /
|
||||||
|
sizeof(increments[0])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* TRAINLOG_SECTION_ASCII_HEADER */
|
/* TRAINLOG_SECTION_ASCII_HEADER */
|
||||||
|
|
||||||
static void section_ascii_header(
|
static void section_ascii_header(
|
||||||
|
|
@ -1560,7 +2062,7 @@ static void screen_exercises(
|
||||||
2,
|
2,
|
||||||
"%.*s",
|
"%.*s",
|
||||||
COLS - 4,
|
COLS - 4,
|
||||||
"Tab zone ↑↓/PgUp/PgDn catalogue ←→ menu Entrée ouvrir a ajouter 0/Home accueil F1-F4 direct b/Échap retour"
|
"Tab zone ↑↓/PgUp/PgDn catalogue ←→ menu Entrée ouvrir m max mesuré a ajouter 0/Home accueil F1-F4 direct b/Échap retour"
|
||||||
);
|
);
|
||||||
|
|
||||||
attroff(
|
attroff(
|
||||||
|
|
@ -1571,7 +2073,7 @@ static void screen_exercises(
|
||||||
} else {
|
} else {
|
||||||
draw_shell(
|
draw_shell(
|
||||||
"TRAINLOG — Exercices",
|
"TRAINLOG — Exercices",
|
||||||
"↑↓ naviguer Entrée performance a ajouter b/Échap retour"
|
"↑↓ naviguer Entrée performance m max mesuré a ajouter b/Échap retour"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1757,6 +2259,17 @@ if (primary_top_nav_activate(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (count > 0U &&
|
||||||
|
(key == 'm' ||
|
||||||
|
key == 'M')) {
|
||||||
|
screen_exercise_measured_max(
|
||||||
|
database,
|
||||||
|
&exercises[selected]
|
||||||
|
);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (count > 0U &&
|
if (count > 0U &&
|
||||||
(key == '\n' ||
|
(key == '\n' ||
|
||||||
key == KEY_ENTER)) {
|
key == KEY_ENTER)) {
|
||||||
|
|
|
||||||
514
tui/tests/test_measured_max.c
Normal file
514
tui/tests/test_measured_max.c
Normal file
|
|
@ -0,0 +1,514 @@
|
||||||
|
/**
|
||||||
|
* @file test_measured_max.c
|
||||||
|
* @brief Explicit measured-max and working-load regression tests.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "trainlog/database.h"
|
||||||
|
#include "trainlog/measured_max.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_exercise(
|
||||||
|
TrainlogDatabase *database,
|
||||||
|
const char *id,
|
||||||
|
const char *name
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
trainlog_database_insert_exercise(
|
||||||
|
database,
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
name,
|
||||||
|
TRAINLOG_TRACKING_REPS
|
||||||
|
) == TRAINLOG_STATUS_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool insert_reps_session(
|
||||||
|
TrainlogDatabase *database,
|
||||||
|
const char *session_id,
|
||||||
|
const char *started_at,
|
||||||
|
TrainlogSessionType session_type,
|
||||||
|
const char *exercise_id,
|
||||||
|
TrainlogLoadMode load_mode,
|
||||||
|
const int *reps,
|
||||||
|
const double *weights,
|
||||||
|
size_t set_count
|
||||||
|
)
|
||||||
|
{
|
||||||
|
TrainlogSetInput sets[4];
|
||||||
|
TrainlogSessionExerciseInput exercise;
|
||||||
|
TrainlogSessionInput session;
|
||||||
|
size_t index;
|
||||||
|
|
||||||
|
if (
|
||||||
|
database == NULL ||
|
||||||
|
session_id == NULL ||
|
||||||
|
started_at == NULL ||
|
||||||
|
exercise_id == NULL ||
|
||||||
|
reps == NULL ||
|
||||||
|
set_count == 0U ||
|
||||||
|
set_count > 4U
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
(void)memset(
|
||||||
|
sets,
|
||||||
|
0,
|
||||||
|
sizeof(sets)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (
|
||||||
|
index = 0U;
|
||||||
|
index < set_count;
|
||||||
|
++index
|
||||||
|
) {
|
||||||
|
sets[index].reps =
|
||||||
|
reps[index];
|
||||||
|
|
||||||
|
if (
|
||||||
|
load_mode !=
|
||||||
|
TRAINLOG_LOAD_NONE
|
||||||
|
) {
|
||||||
|
if (weights == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sets[index].has_weight = true;
|
||||||
|
sets[index].weight_kg =
|
||||||
|
weights[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(void)memset(
|
||||||
|
&exercise,
|
||||||
|
0,
|
||||||
|
sizeof(exercise)
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
exercise.exercise_id,
|
||||||
|
sizeof(exercise.exercise_id),
|
||||||
|
"%s",
|
||||||
|
exercise_id
|
||||||
|
);
|
||||||
|
|
||||||
|
exercise.recording_mode =
|
||||||
|
TRAINLOG_RECORDING_SETS;
|
||||||
|
|
||||||
|
exercise.load_mode =
|
||||||
|
load_mode;
|
||||||
|
|
||||||
|
exercise.rest_seconds = 120;
|
||||||
|
exercise.target_sets =
|
||||||
|
(int)set_count;
|
||||||
|
exercise.target_reps = 1;
|
||||||
|
|
||||||
|
if (
|
||||||
|
load_mode !=
|
||||||
|
TRAINLOG_LOAD_NONE
|
||||||
|
) {
|
||||||
|
exercise.target_has_weight = true;
|
||||||
|
exercise.target_weight_kg =
|
||||||
|
weights[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
exercise.sets = sets;
|
||||||
|
exercise.set_count = set_count;
|
||||||
|
|
||||||
|
(void)memset(
|
||||||
|
&session,
|
||||||
|
0,
|
||||||
|
sizeof(session)
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
session.session_id,
|
||||||
|
sizeof(session.session_id),
|
||||||
|
"%s",
|
||||||
|
session_id
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
session.started_at,
|
||||||
|
sizeof(session.started_at),
|
||||||
|
"%s",
|
||||||
|
started_at
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
session.ended_at,
|
||||||
|
sizeof(session.ended_at),
|
||||||
|
"%s",
|
||||||
|
"2026-09-06T21:00:00+02:00"
|
||||||
|
);
|
||||||
|
|
||||||
|
session.session_type =
|
||||||
|
session_type;
|
||||||
|
|
||||||
|
session.exercises =
|
||||||
|
&exercise;
|
||||||
|
|
||||||
|
session.exercise_count = 1U;
|
||||||
|
|
||||||
|
return
|
||||||
|
trainlog_database_insert_session(
|
||||||
|
database,
|
||||||
|
&session
|
||||||
|
) == TRAINLOG_STATUS_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test_measured_max_semantics(void)
|
||||||
|
{
|
||||||
|
TrainlogDatabase *database = NULL;
|
||||||
|
|
||||||
|
TrainlogExercisePerformancePoint
|
||||||
|
points[16];
|
||||||
|
|
||||||
|
TrainlogMeasuredMaxSummary summary;
|
||||||
|
|
||||||
|
size_t count = 0U;
|
||||||
|
double working = 0.0;
|
||||||
|
|
||||||
|
const int one_rep[] = {1};
|
||||||
|
const int failed_then_one[] = {0, 1};
|
||||||
|
const int reps_20[] = {20};
|
||||||
|
const int reps_15[] = {15};
|
||||||
|
|
||||||
|
const double training_140[] = {140.0};
|
||||||
|
const double external_110[] = {110.0};
|
||||||
|
const double external_latest[] = {
|
||||||
|
115.0,
|
||||||
|
105.0,
|
||||||
|
};
|
||||||
|
const double assistance_30[] = {30.0};
|
||||||
|
const double assistance_35[] = {35.0};
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_open(
|
||||||
|
":memory:",
|
||||||
|
&database
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
add_exercise(
|
||||||
|
database,
|
||||||
|
"ex_measured_external",
|
||||||
|
"measured external"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
add_exercise(
|
||||||
|
database,
|
||||||
|
"ex_measured_assistance",
|
||||||
|
"measured assistance"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
add_exercise(
|
||||||
|
database,
|
||||||
|
"ex_measured_none",
|
||||||
|
"measured none"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A stronger ordinary training set must never become a measured maximum.
|
||||||
|
*/
|
||||||
|
CHECK(
|
||||||
|
insert_reps_session(
|
||||||
|
database,
|
||||||
|
"se_training_140",
|
||||||
|
"2026-09-01T18:00:00+02:00",
|
||||||
|
TRAINLOG_SESSION_TRAINING,
|
||||||
|
"ex_measured_external",
|
||||||
|
TRAINLOG_LOAD_EXTERNAL,
|
||||||
|
one_rep,
|
||||||
|
training_140,
|
||||||
|
1U
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
insert_reps_session(
|
||||||
|
database,
|
||||||
|
"se_max_external_old",
|
||||||
|
"2026-09-02T18:00:00+02:00",
|
||||||
|
TRAINLOG_SESSION_MAX_TEST,
|
||||||
|
"ex_measured_external",
|
||||||
|
TRAINLOG_LOAD_EXTERNAL,
|
||||||
|
one_rep,
|
||||||
|
external_110,
|
||||||
|
1U
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 115 kg x 0 is a failed attempt. The newest successful measured result is
|
||||||
|
* 105 kg while the older historical measured record remains 110 kg.
|
||||||
|
*/
|
||||||
|
CHECK(
|
||||||
|
insert_reps_session(
|
||||||
|
database,
|
||||||
|
"se_max_external_new",
|
||||||
|
"2026-09-03T18:00:00+02:00",
|
||||||
|
TRAINLOG_SESSION_MAX_TEST,
|
||||||
|
"ex_measured_external",
|
||||||
|
TRAINLOG_LOAD_EXTERNAL,
|
||||||
|
failed_then_one,
|
||||||
|
external_latest,
|
||||||
|
2U
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_list_exercise_performance(
|
||||||
|
database,
|
||||||
|
"ex_measured_external",
|
||||||
|
points,
|
||||||
|
16U,
|
||||||
|
&count
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(count == 3U);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
points[0].session_type ==
|
||||||
|
TRAINLOG_SESSION_MAX_TEST
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
points[2].session_type ==
|
||||||
|
TRAINLOG_SESSION_TRAINING
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_measured_max_summarize(
|
||||||
|
points,
|
||||||
|
count,
|
||||||
|
&summary
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(summary.test_count == 2U);
|
||||||
|
CHECK(summary.successful_test_count == 2U);
|
||||||
|
CHECK(summary.found);
|
||||||
|
CHECK(summary.has_record);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
strcmp(
|
||||||
|
summary.current.session_id,
|
||||||
|
"se_max_external_new"
|
||||||
|
) == 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
summary.current.weight_kg >
|
||||||
|
104.99
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
summary.current.weight_kg <
|
||||||
|
105.01
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
summary.current.metric_value ==
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
summary.record.weight_kg >
|
||||||
|
109.99
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
summary.record.weight_kg <
|
||||||
|
110.01
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_measured_max_working_load(
|
||||||
|
&summary.current,
|
||||||
|
80.0,
|
||||||
|
2.5,
|
||||||
|
&working
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
working > 84.99 &&
|
||||||
|
working < 85.01
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
insert_reps_session(
|
||||||
|
database,
|
||||||
|
"se_max_assistance_old",
|
||||||
|
"2026-09-02T19:00:00+02:00",
|
||||||
|
TRAINLOG_SESSION_MAX_TEST,
|
||||||
|
"ex_measured_assistance",
|
||||||
|
TRAINLOG_LOAD_ASSISTANCE,
|
||||||
|
one_rep,
|
||||||
|
assistance_30,
|
||||||
|
1U
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
insert_reps_session(
|
||||||
|
database,
|
||||||
|
"se_max_assistance_new",
|
||||||
|
"2026-09-03T19:00:00+02:00",
|
||||||
|
TRAINLOG_SESSION_MAX_TEST,
|
||||||
|
"ex_measured_assistance",
|
||||||
|
TRAINLOG_LOAD_ASSISTANCE,
|
||||||
|
one_rep,
|
||||||
|
assistance_35,
|
||||||
|
1U
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_list_exercise_performance(
|
||||||
|
database,
|
||||||
|
"ex_measured_assistance",
|
||||||
|
points,
|
||||||
|
16U,
|
||||||
|
&count
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_measured_max_summarize(
|
||||||
|
points,
|
||||||
|
count,
|
||||||
|
&summary
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
summary.current.weight_kg >
|
||||||
|
34.99
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
summary.current.weight_kg <
|
||||||
|
35.01
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
summary.record.weight_kg >
|
||||||
|
29.99
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
summary.record.weight_kg <
|
||||||
|
30.01
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_measured_max_working_load(
|
||||||
|
&summary.current,
|
||||||
|
80.0,
|
||||||
|
2.5,
|
||||||
|
&working
|
||||||
|
) == TRAINLOG_STATUS_INVALID_ARGUMENT
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
insert_reps_session(
|
||||||
|
database,
|
||||||
|
"se_max_none_old",
|
||||||
|
"2026-09-02T20:00:00+02:00",
|
||||||
|
TRAINLOG_SESSION_MAX_TEST,
|
||||||
|
"ex_measured_none",
|
||||||
|
TRAINLOG_LOAD_NONE,
|
||||||
|
reps_20,
|
||||||
|
NULL,
|
||||||
|
1U
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
insert_reps_session(
|
||||||
|
database,
|
||||||
|
"se_max_none_new",
|
||||||
|
"2026-09-03T20:00:00+02:00",
|
||||||
|
TRAINLOG_SESSION_MAX_TEST,
|
||||||
|
"ex_measured_none",
|
||||||
|
TRAINLOG_LOAD_NONE,
|
||||||
|
reps_15,
|
||||||
|
NULL,
|
||||||
|
1U
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_list_exercise_performance(
|
||||||
|
database,
|
||||||
|
"ex_measured_none",
|
||||||
|
points,
|
||||||
|
16U,
|
||||||
|
&count
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_measured_max_summarize(
|
||||||
|
points,
|
||||||
|
count,
|
||||||
|
&summary
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
summary.current.metric_value ==
|
||||||
|
15
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
summary.record.metric_value ==
|
||||||
|
20
|
||||||
|
);
|
||||||
|
|
||||||
|
trainlog_database_close(
|
||||||
|
database
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
CHECK(
|
||||||
|
test_measured_max_semantics()
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)printf(
|
||||||
|
"PASS measured_max\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue