v1.2.4 : traitement inactif = drapeau administratif, jamais un filtre de données

Bug remonté : passer un traitement à inactif effaçait sa simulation du graphique
(Home/Chart collectaient activeTreatments) et faussait la calibration, tout en
permettant d'y loger des doses.

- HomeScreen / ChartScreen : simulation + calibration avec ALLTreatments
  (actifs + inactifs) — l'historique EV d'un traitement archivé reste simulé
- Home : chips « Log rapide », dialog de création et carte « prochaine dose »
  sur les traitements ACTIFS uniquement
- DosesScreen : création → actifs uniquement ; édition d'une dose existante →
  tous les traitements (rattachement préservé)
- Rappels : annulés pour les inactifs (save, boot via getAllOnce, prochaine dose)
- RegressionUserCase3Test (6 tests, données réelles HORS dépôt gitignoré) :
  transition complète EV inactif (29 doses) → EEn actif (9 doses), 22 labs sur
  les deux périodes, calibration par période couvrant EV + EEN, k T par ester,
  continuité de courbe, niveau actuel — 62 tests verts
- docs : §6.bis (sémantique isActive), §14 bug #32, §8, §21, CHANGELOG,
  README, GUIDE ; versionCode 7, versionName 1.2.4
This commit is contained in:
Siphonight 2026-09-05 20:05:07 +02:00
parent 9301b96902
commit 3f9b2bd8a0
12 changed files with 251 additions and 19 deletions

View File

@ -9,7 +9,7 @@ sauvegarde JSON. **100 % local, aucun compte, aucun serveur.**
> à titre informatif — ce ne sont pas des mesures. Fie-toi toujours à tes analyses de
> sang et aux consignes de ton endocrinologue.
- **Statut** : v1.2.3 — build Android ✅, **56 tests unitaires** ✅ (2 régressions épinglées sur données réelles), intégration montre = notifications ✅, **repo git avec releases taguées** ✅
- **Statut** : v1.2.4 — build Android ✅, **62 tests unitaires** ✅ (44 sans les données de test locales ; 3 régressions épinglées sur données réelles **non versionnées**), intégration montre = notifications ✅, **dépôt Gitea privé + releases avec APK** ✅
- **Journal des versions** : [docs/CHANGELOG.md](docs/CHANGELOG.md)
- **Guide utilisateur** : [docs/GUIDE_INSTALLATION.md](docs/GUIDE_INSTALLATION.md)
- **Doc de développement** (architecture, maths, décisions, bugs) : [docs/DEVELOPPEMENT.md](docs/DEVELOPPEMENT.md)
@ -30,6 +30,9 @@ sauvegarde JSON. **100 % local, aucun compte, aucun serveur.**
- **Log des doses** avec date/heure exacte, dose en mg, **ester par injection**
(switch EV↔EU↔EEn comme dans le tableur), **éditable** (tap sur une ligne dans Doses),
**intervalle en jours entre dosages** affiché
- **Traitements inactifs** : un traitement archivé n'apparaît plus dans la saisie
ni dans les rappels, mais **tout son historique reste simulé et calibré** —
utile pour une transition valerate → enanthate
- **Analyses de sang** : **E2 + T en une seule entrée** (chacune optionnelle), affichées
**côte à côte** quand elles partagent la même date/heure, éditables (tap → choix de
l'entrée) ; unités T : ng/mL, ng/dL, ng/L, nmol/L

View File

@ -12,8 +12,8 @@ android {
applicationId = "com.hormonetrack"
minSdk = 26
targetSdk = 36
versionCode = 6
versionName = "1.2.3"
versionCode = 7
versionName = "1.2.4"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

View File

@ -556,7 +556,8 @@ object PharmacokineticEngine {
var next: Long? = null
val cal = java.util.Calendar.getInstance()
for (tr in treatments) {
if (!tr.reminderEnabled) continue
// Un traitement inactif ne génère plus de rappels
if (!tr.reminderEnabled || !tr.isActive) continue
val h = tr.reminderHour ?: continue
val m = tr.reminderMinute ?: continue
cal.timeInMillis = nowMs

View File

@ -114,7 +114,8 @@ class AlarmScheduler(private val context: Context) {
fun rescheduleAll(treatments: List<Treatment>) {
treatments.forEach { tr ->
if (tr.reminderEnabled) {
// Un traitement inactif ne rappelle plus : son alarme est annulée
if (tr.reminderEnabled && tr.isActive) {
scheduleDaily(tr)
} else {
cancel(tr.id)
@ -211,7 +212,9 @@ class BootReceiver : BroadcastReceiver() {
Thread {
try {
val treatments = kotlinx.coroutines.runBlocking {
AppDatabase.getInstance(context).treatmentDao().getActiveOnce()
// TOUS les traitements : les inactifs doivent voir leur alarme
// annulée au boot (sinon une alarme périmée continue de sonner)
AppDatabase.getInstance(context).treatmentDao().getAllOnce()
}
AlarmScheduler(context).rescheduleAll(treatments)
} finally {

View File

@ -70,7 +70,10 @@ private const val DAY_MS = 24 * HOUR_MS
fun ChartScreen() {
val container = LocalAppContainer.current
val repo = container.repository
val treatments by repo.activeTreatments.collectAsState(initial = emptyList())
// ⚠️ TOUS les traitements (actifs ET inactifs) : l'inactivation d'un traitement
// ne doit jamais retirer ses doses de la simulation ni de la calibration
// (drapeau administratif, pas filtre de données — cf bug v1.2.4).
val treatments by repo.allTreatments.collectAsState(initial = emptyList())
val doseLogs by repo.allDoseLogs.collectAsState(initial = emptyList())
val labResults by repo.allLabResults.collectAsState(initial = emptyList())
val tConfig by container.settings.tConfig.collectAsState(initial = TConfig())

View File

@ -156,7 +156,10 @@ fun DosesScreen() {
if (showLogDialog || toEdit != null) {
DoseDialog(
treatments = treatments,
// Création : traitements ACTIFS uniquement (le dropdown ne propose pas
// un traitement passé à inactif). Édition : TOUS les traitements — la
// dose éditée doit rester rattachable à son traitement même inactif.
treatments = if (toEdit != null) treatments else treatments.filter { it.isActive },
preselectedTreatmentId = null,
existing = toEdit,
onDismiss = {

View File

@ -82,7 +82,12 @@ fun HomeScreen(
) {
val container = LocalAppContainer.current
val repo = container.repository
val treatments by repo.activeTreatments.collectAsState(initial = emptyList())
// ⚠️ TOUS les traitements (actifs ET inactifs) pour la simulation : « inactif »
// est un drapeau administratif (plus de nouvelles doses), jamais un filtre de
// données — l'historique EV d'un traitement passé à inactif doit continuer
// d'être simulé et calibré (bug v1.2.4).
val allTreatments by repo.allTreatments.collectAsState(initial = emptyList())
val activeTreatments = remember(allTreatments) { allTreatments.filter { it.isActive } }
val doseLogs by repo.allDoseLogs.collectAsState(initial = emptyList())
val labResults by repo.allLabResults.collectAsState(initial = emptyList())
val tConfig by container.settings.tConfig.collectAsState(initial = TConfig())
@ -92,13 +97,13 @@ fun HomeScreen(
// recalculés à la volée depuis les labs. null = option désactivée (échelles
// stockées par traitement). Copies uniquement : rien n'est persisté.
val effectiveAuto by produceState<PharmacokineticEngine.AutoCalibrated?>(
null, treatments, doseLogs, labResults, tConfig, autoCalibrate
null, allTreatments, doseLogs, labResults, tConfig, autoCalibrate
) {
value = if (!autoCalibrate) {
null
} else {
withContext(Dispatchers.Default) {
PharmacokineticEngine.autoCalibrated(treatments, doseLogs, labResults, tConfig)
PharmacokineticEngine.autoCalibrated(allTreatments, doseLogs, labResults, tConfig)
}
}
}
@ -125,7 +130,7 @@ fun HomeScreen(
withContext(Dispatchers.Default) {
val end = System.currentTimeMillis()
value = PharmacokineticEngine.computeCurve(
effectiveAuto?.treatments ?: treatments,
effectiveAuto?.treatments ?: allTreatments,
doseLogs,
startMs = end - 24 * HOUR_MS,
endMs = end,
@ -167,7 +172,7 @@ fun HomeScreen(
NowLevelCard(curve)
NextDoseCard(treatments)
NextDoseCard(activeTreatments)
Card(
modifier = Modifier.fillMaxWidth(),
@ -183,7 +188,7 @@ fun HomeScreen(
Modifier.horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
treatments.forEach { tr ->
activeTreatments.forEach { tr ->
AssistChip(
onClick = {
preselectId = tr.id
@ -261,7 +266,7 @@ fun HomeScreen(
if (showLogDialog) {
DoseDialog(
treatments = treatments,
treatments = activeTreatments,
preselectedTreatmentId = preselectId,
existing = null,
onDismiss = { showLogDialog = false },

View File

@ -421,7 +421,9 @@ fun TreatmentEditorScreen(treatmentId: Long, onDone: () -> Unit) {
repo.insertTreatment(tr)
}
val saved = tr.copy(id = savedId)
if (saved.reminderEnabled) {
// Rappel actif seulement si le traitement l'est aussi ;
// désactiver un traitement annule son alarme
if (saved.reminderEnabled && saved.isActive) {
scheduler.scheduleDaily(saved)
} else {
scheduler.cancel(savedId)

View File

@ -0,0 +1,146 @@
package com.hormonetrack.pk
import com.google.gson.Gson
import com.hormonetrack.data.backup.BackupData
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Test
import java.io.File
/**
* Régression épinglée sur le **3ᵉ export réel** (v1.2.3) — le scénario complet de
* transition : 2 traitements, **EV inactif** (« Injection EV - old », 29 doses de
* fin janvier à début juillet, doses 2–8 mg) puis **EEn actif** (9 doses, 5 mg
* tous les ~6-7 j), 22 labs couvrant les DEUX périodes.
*
* ⚠️ Données de santé personnelles : fichier HORS dépôt
* (`local-test-data/backup-v1.2.3.json`, gitignoré) ; tests ignorés si absent.
*
* Cas de bug v1.2.4 : passer un traitement à `isActive = false` faisait disparaître
* sa simulation (et faussait la calibration) — « inactif » est un drapeau
* administratif (saisie), JAMAIS un filtre de données. Ce test épinglle le
* contrat : **le moteur reçoit TOUS les traitements et simule tout l'historique**.
*/
class RegressionUserCase3Test {
companion object {
private val DATA_FILE: File = listOf(
File("../local-test-data/backup-v1.2.3.json"),
File("local-test-data/backup-v1.2.3.json"),
File("app/local-test-data/backup-v1.2.3.json")
).firstOrNull { it.exists() } ?: File("../local-test-data/backup-v1.2.3.json")
}
@Before
fun setup() {
assumeTrue(
"local-test-data/backup-v1.2.3.json absent — tests ignorés (données locales)",
DATA_FILE.exists()
)
if (!PKProfileStore.hasProfile("EV", "TFS")) {
val file = listOf(
File("src/main/assets/pk_profiles.json"),
File("app/src/main/assets/pk_profiles.json")
).first { it.exists() }
PKProfileStore.initWithJson(file.readText())
}
}
private fun importUserBackup(): BackupData =
Gson().fromJson(DATA_FILE.readText(), BackupData::class.java)
@Test
fun `third backup parses with an inactive EV treatment and an active EEn one`() {
val data = importUserBackup()
assertEquals(2, data.treatments.size)
assertEquals(38, data.doseLogs.size)
assertEquals(22, data.labResults.size)
val ev = data.treatments.single { it.esterType == "EV" }
val een = data.treatments.single { it.esterType == "EEN" }
// Le traitement EV est INACTIF : ses doses doivent néanmoins être simulées
assertTrue(!ev.isActive)
assertTrue(een.isActive)
assertTrue(data.doseLogs.count { it.treatmentId == ev.id } > 20)
assertTrue(data.doseLogs.count { it.treatmentId == een.id } > 5)
}
@Test
fun `inactive treatment doses are still simulated (bug v1_2_4 contract)`() {
val data = importUserBackup()
// La fenêtre couvre la PÉRIODE EV : si le code consommateur filtrait sur
// isActive, la courbe serait vide ici — le moteur, lui, simule tout ce
// qu'on lui passe (les écrans passent TOUS les traitements, cf §10).
val evTreatment = data.treatments.single { it.esterType == "EV" }
val evDoses = data.doseLogs.filter { it.treatmentId == evTreatment.id }
val firstEv = evDoses.minOf { it.timestamp }
val curve = PharmacokineticEngine.computeCurve(
data.treatments, data.doseLogs,
startMs = firstEv,
endMs = firstEv + 48 * PharmacokineticEngine.HOUR_MS,
tConfig = data.tConfig
)
assertTrue(curve.isNotEmpty())
assertTrue("e2=${curve.last().e2} pendant la période EV (traitement inactif)", curve.last().e2 > 50.0)
}
@Test
fun `per-ester calibration covers BOTH periods (EV and EEn)`() {
val data = importUserBackup()
val scales = PharmacokineticEngine.computeEsterScaleFactors(
data.treatments, data.doseLogs,
data.labResults.filter { it.marker.equals("E2", true) }
)
// Les labs des 2 périodes (valerate ET enanthate) calibrent chacune leur ester
assertEquals(setOf("EV", "EEN"), scales.keys)
// EV : facteur plausible (l'utilisatrice avait calibré manuellement à 1,4)
assertTrue("EV=${scales["EV"]}", scales["EV"]!! in 0.5..2.5)
assertTrue("EEN=${scales["EEN"]}", scales["EEN"]!! in 0.2..2.0)
}
@Test
fun `T calibration k is also per ester period`() {
val data = importUserBackup()
val result = PharmacokineticEngine.autoCalibrated(
data.treatments, data.doseLogs, data.labResults, data.tConfig
)
assertTrue(result.tKPerEster.keys.containsAll(setOf("EV", "EEN")))
result.tKPerEster.values.forEach { k ->
assertTrue("k=$k", k in 0.001..1.0)
}
// Les labs des deux périodes existent (7 labs T en période EV, 4 en EEn)
val tLabs = data.labResults.filter { it.marker.equals("T", true) }
assertTrue(tLabs.size >= 10)
}
@Test
fun `current level at export time reflects the active EEn treatment`() {
val data = importUserBackup()
val point = PharmacokineticEngine.levelAt(
data.treatments, data.doseLogs, data.exportedAt, data.tConfig
)
// ~122 h après 5 mg EEn (TFS) × SF 0,73 → état d'équilibre ~240-280 pg/mL
assertTrue("e2=${point.e2}", point.e2 in 200.0..350.0)
// k stocké 0,068 → T ≈ 0,2 + 5,8/(1+0,068×268) ≈ 0,48
assertTrue("t=${point.t}", point.t in 0.35..0.6)
}
@Test
fun `transition window superposes the tail of EV and the start of EEn`() {
val data = importUserBackup()
// Fenêtre à cheval sur la transition (EV dernier dose début juillet,
// EEn première dose ~5 j après) : la courbe doit rester continue
val curve = PharmacokineticEngine.computeCurve(
data.treatments, data.doseLogs,
startMs = data.exportedAt - 70 * 24 * PharmacokineticEngine.HOUR_MS,
endMs = data.exportedAt - 60 * 24 * PharmacokineticEngine.HOUR_MS,
tConfig = data.tConfig
)
assertTrue(curve.isNotEmpty())
// Pas de trou : chaque point de la fenêtre a un E2 > 0
curve.forEach { p ->
assertTrue("e2=0 à ${p.timestamp} — trou de simulation pendant la transition", p.e2 > 0.0)
}
}
}

View File

@ -1,7 +1,31 @@
# Changelog — HormoneTrack
Format : [Keep a Changelog](https://keepachangelog.com/fr-FR/1.1.0/).
Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.2.3`, …).
Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.2.4`, …).
## [1.2.4] — 2026-09-05 (versionCode 7)
### Corrigé
- **Traitement inactif = drapeau administratif, pas filtre de données** (bug
remonté) : passer un traitement à inactif faisait disparaître sa simulation du
graphique **et** sa contribution à la calibration, tout en permettant encore
d'y loger des doses (incohérence). Désormais :
- Home et Graphiques simulent avec **TOUS les traitements** (actifs + inactifs)
→ l'historique EV d'un traitement passé à inactif reste simulé et calibré ;
- l'inactivation retire uniquement : les chips « Log rapide », le traitement
dans le dropdown des **nouvelles** doses (l'édition d'une dose existante
garde son traitement même inactif), et les **rappels** (alarme annulée au
save, au boot et dans le calcul de la prochaine dose) ;
- la suppression d'une dose reste possible depuis l'historique.
### Ajouté
- **Régression n°3** épinglée sur le 3ᵉ export réel (HORS dépôt, gitignoré) :
le scénario complet de transition — 1 traitement **EV inactif** (29 doses,
2–8 mg, janvier→juillet) + 1 traitement **EEn actif** (9 doses), 22 labs
couvrant les deux périodes. Vérifie : simulation de l'inactif, calibration
par période sur les DEUX esters (EV **et** EEN), k T par ester, continuité
de la courbe pendant la transition, niveau actuel. 6 nouveaux tests (62 au
total, tous verts).
## [1.2.3] — 2026-09-05 (versionCode 6)

View File

@ -64,6 +64,7 @@ Fonctionnalités v1 :
| 5 sept. 2026 (session tests/docs) | Correction de toutes les erreurs de compilation (dont 3 vrais bugs logiciels trouvés par les tests), **24 tests unitaires verts**, APK debug généré (18 MB), documentation complète (README + docs/), préparation repo git. |
| 5 sept. 2026 (session v1.1.0) | Bugs remontés par l'utilisatrice : courbes vides (casse EEn) + pas d'édition des doses → corrigés ; régression épinglée sur ses données réelles ; APK v1.1.0. |
| 5 sept. 2026 (session v1.2.0) | **Repo git initialisé** (commits par couche + tags de release) ; montée toolchain AGP 9.4/Gradle 9.7.1/Kotlin 2.3.21/BOM 2026.08.00/compileSdk 37 ; panoramique du chart, superposition des deux modèles, prévision par « Fréquence », calibration automatique optionnelle, intervalles entre doses, TimePicker centré ; **36 tests verts**, APK v1.2.0 (23 MB). |
| 5 sept. 2026 (session v1.2.4) | Bug `isActive` (simulation effacée) → sémantique « drapeau administratif » (§6.bis) ; régression n°3 sur le 3ᵉ export réel (transition EV inactif → EEn actif, 38 doses, 22 labs) ; **62 tests verts**, APK v1.2.4. |
| 5 sept. 2026 (session push Gitea + v1.2.3) | Push initial vers **gitea.cloudyfy.fr/Siphonight/HormoneTrack** (privé) après **anonymisation de l'historique** (filter-branch : les premiers commits embarquaient les valeurs réelles des tests) ; **données de test réelles déplacées hors dépôt** (`local-test-data/` gitignoré, tests `Assume`-skippés) ; **releases avec APK en pièce jointe** ; pics/creux + calibration T par période d'ester ; **56 tests verts**. |
| 5 sept. 2026 (session v1.2.1) | Remontées UX/utilisateur : calibration **par période d'ester** (labs EV → doses EV, labs EEn → doses EEn — corrige les courbes gonflées), lab T "pg/mL" neutralisé, **édition des labs**, tap accueil → Graphiques + mini-légende, auto-cal fusionnée (E2+T, un seul switch) ; 2ᵉ régression épinglée sur le nouvel export ; **44 tests verts**, APK v1.2.1. |
@ -178,6 +179,26 @@ l'utilisatrice tape autre chose, la calibration ignorera ces labs.
`Flow` pour l'UI + one-shots `suspend *Once()` pour backup/boot/calibration :
`TreatmentDao.getActiveOnce/getAllOnce`, `DoseLogDao.getAllOnce`, `LabResultDao.getAllOnce`.
### 6.bis Sémantique `isActive` (v1.2.4) — drapeau administratif, JAMAIS un filtre
`Treatment.isActive = false` signifie « ce traitement ne produit plus de nouvelles
doses ». **Ce n'est PAS un filtre de données** — passer un traitement à inactif ne
doit rien effacer (bug v1.2.4 : Home/Chart collectaient `activeTreatments`, la
simulation de tout l'historique EV disparaissait) :
| Effet de l'inactivation | Où |
|---|---|
| ✅ Retiré des chips « Log rapide » (accueil) | `HomeScreen` → `activeTreatments` |
| ✅ Retiré du dropdown des **nouvelles** doses | `DosesScreen` : création → actifs only ; **édition → tous** (une dose existante reste rattachée à son traitement inactif) |
| ✅ Rappels annulés (au save, au boot, au calcul de « prochaine dose ») | `rescheduleAll` + `nextReminderFireMs` (filtre `isActive`) ; `BootReceiver` lit `getAllOnce()` pour annuler les alarmes des inactifs |
| ❌ JAMAIS retiré de la simulation | Home/Chart passent **`allTreatments`** au moteur — les écrans ne filtrent jamais |
| ❌ JAMAIS retiré de la calibration (auto) | `autoCalibrated(allTreatments, …)` |
| ❌ JAMAIS supprimé de l'historique des doses | `DosesScreen` liste tout, suppression manuelle uniquement |
Épinglé par la régression n°3 (`RegressionUserCase3Test`, cf §8) : EV inactif + EEn
actif en même temps, les 22 labs couvrant les deux périodes doivent rester simulés
et calibrés.
## 7. Moteur pharmacocinétique
`pk/PharmacokineticEngine.kt` + `pk/PKProfileStore.kt`.
@ -309,7 +330,8 @@ Type de retour : `LevelPoint(timestamp, e2, t)`.
## 8. Tests unitaires
**56 tests JVM, tous verts** (`./gradlew testDebugUnitTest`). Dépendance : JUnit 4.13.2.
**62 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — 44 sans les
données de test locales (cf §8.bis). Dépendance : JUnit 4.13.2.
Emplacement : `app/src/test/java/com/hormonetrack/`. Répertoire de travail d'exécution =
`app/` → l'asset est lu via `src/main/assets/pk_profiles.json` (fallback `app/src/…`).
@ -346,6 +368,12 @@ Emplacement : `app/src/test/java/com/hormonetrack/`. Répertoire de travail d'ex
non calibré ≈ 367 = les « 375 » rapportés), facteur unique EEN plausible, lab T en
unité aberrante neutralisé, prévision 6 j exacte, auto-cal cohérente.
**Tout nouvel export utilisateur = un nouveau test de régression.**
- **`RegressionUserCase3Test`** (6) : 3ᵉ régression (export v1.2.3) — le scénario
**transition** : traitement **EV inactif** (29 doses 2–8 mg, janvier→juillet) +
traitement **EEn actif** (9 doses), 22 labs sur les deux périodes. Vérifie :
parsing, **l'inactif reste simulé** (bug v1.2.4 — le moteur reçoit TOUS les
traitements), calibration par période couvrant EV **et** EEN, k T par ester,
continuité de la courbe pendant la transition, niveau actuel ~EEn équilibre.
### 8.bis Données de test réelles : HORS dépôt (`local-test-data/`)
@ -588,6 +616,13 @@ Historique complet — **à ne pas réintroduire** (utile pour diff/revert) :
titre `[1.2.3]` : l'extraction cherchait `[vX.Y.Z]` → fallback silencieux.
→ `scripts/gitea-release.py` (corps = section CHANGELOG, APK attaché).
32. **`isActive` traité comme filtre de données** (v1.2.4) : passer un
traitement à inactif effaçait sa simulation du graphique (écrans
collectaient `activeTreatments`) tout en permettant d'y loger des doses.
→ sémantique documentée §6.bis : l'inactivation ne touche que la SAISIE
(chips, dropdown création, rappels) ; simulation et calibration reçoivent
TOUS les traitements. Épinglé par la régression n°3.
**Leçons** : (a) ne jamais croire un build « probablement bon » sans l'avoir lancé ;
(b) les tests sémantiques attrapent ce que la compilation ne voit pas ; (c) se méfier des
constantes stdlib « de mémoire » (`ln2`), des mélanges Float/Double, et des APIs M3
@ -779,6 +814,9 @@ Sur le téléphone de test (à compléter par l'utilisatrice) :
- [ ] **v1.2.x** : édition d'une dose (tap ligne Doses) et d'un lab (tap ligne
Analyses → sélecteur E2/T si paire) ; suppression par prise entière
- [ ] **v1.2.x** : prise de sang E2 + T en une entrée (champs optionnels)
- [ ] **v1.2.4** : passer un traitement à inactif → retiré de « Log rapide » et
du dropdown des nouvelles doses, rappel annulé, **mais sa simulation reste
sur le graphique** et la calibration couvre toujours ses périodes
---
*Doc mise à jour le 5 sept. 2026 (v1.2.3) — build OK, 56/56 tests verts (44 sans les données locales), dépôt Gitea privé + releases APK, aucune donnée de santé dans le dépôt ni l'historique.*
*Doc mise à jour le 5 sept. 2026 (v1.2.4) — build OK, 62/62 tests verts (44 sans les données locales), dépôt Gitea privé + releases APK, aucune donnée de santé dans le dépôt ni l'historique.*

View File

@ -58,6 +58,10 @@ Les notifications de l'app remontent automatiquement sur la montre via **Huawei
## 6. Premiers pas dans l'app
1. **Traitements → +** → choisis un preset (ex : *Injection EV — Estrannaise*)
- Tu as changé d'ester ? Crée (ou garde) le traitement de l'ancien ester et
passe-le **inactif** (switch « Actif ») : il disparaît de la saisie et de tes
rappels, mais **son historique reste simulé et calibré** sur les graphiques —
parfait pour une transition valerate → enanthate
- Ester (EV / EU / EEn) + modèle (Estrannaise / Transfem Science) = les courbes du `.ods`
- Pour gel/patch/oral : paramètres Bateman (temps au pic, demi-vie, biodispo)
2. **Doses → +** → logue tes injections passées (date/heure exactes, dose en mg)