From 5d04667ea9887daea86aefd5bf16c0942d53608a Mon Sep 17 00:00:00 2001 From: Siphonight Date: Sat, 5 Sep 2026 20:05:07 +0200 Subject: [PATCH] =?UTF-8?q?v1.2.4=20:=20traitement=20inactif=20=3D=20drape?= =?UTF-8?q?au=20administratif,=20jamais=20un=20filtre=20de=20donn=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- README.md | 5 +- app/build.gradle.kts | 4 +- .../hormonetrack/pk/PharmacokineticEngine.kt | 3 +- .../hormonetrack/reminder/ReminderManager.kt | 7 +- .../hormonetrack/ui/screens/ChartScreen.kt | 5 +- .../hormonetrack/ui/screens/DosesScreen.kt | 5 +- .../com/hormonetrack/ui/screens/HomeScreen.kt | 19 ++- .../ui/screens/TreatmentEditorScreen.kt | 4 +- .../pk/RegressionUserCase3Test.kt | 146 ++++++++++++++++++ docs/CHANGELOG.md | 26 +++- docs/DEVELOPPEMENT.md | 42 ++++- docs/GUIDE_INSTALLATION.md | 4 + 12 files changed, 251 insertions(+), 19 deletions(-) create mode 100644 app/src/test/java/com/hormonetrack/pk/RegressionUserCase3Test.kt diff --git a/README.md b/README.md index 9b3aa2a..1517580 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1872f87..639e469 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 { diff --git a/app/src/main/java/com/hormonetrack/pk/PharmacokineticEngine.kt b/app/src/main/java/com/hormonetrack/pk/PharmacokineticEngine.kt index 26dfb68..b0b6834 100644 --- a/app/src/main/java/com/hormonetrack/pk/PharmacokineticEngine.kt +++ b/app/src/main/java/com/hormonetrack/pk/PharmacokineticEngine.kt @@ -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 diff --git a/app/src/main/java/com/hormonetrack/reminder/ReminderManager.kt b/app/src/main/java/com/hormonetrack/reminder/ReminderManager.kt index f0d194c..7d6bf31 100644 --- a/app/src/main/java/com/hormonetrack/reminder/ReminderManager.kt +++ b/app/src/main/java/com/hormonetrack/reminder/ReminderManager.kt @@ -114,7 +114,8 @@ class AlarmScheduler(private val context: Context) { fun rescheduleAll(treatments: List) { 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 { diff --git a/app/src/main/java/com/hormonetrack/ui/screens/ChartScreen.kt b/app/src/main/java/com/hormonetrack/ui/screens/ChartScreen.kt index fc19a1f..7bce7f8 100644 --- a/app/src/main/java/com/hormonetrack/ui/screens/ChartScreen.kt +++ b/app/src/main/java/com/hormonetrack/ui/screens/ChartScreen.kt @@ -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()) diff --git a/app/src/main/java/com/hormonetrack/ui/screens/DosesScreen.kt b/app/src/main/java/com/hormonetrack/ui/screens/DosesScreen.kt index ecbeec2..7b52091 100644 --- a/app/src/main/java/com/hormonetrack/ui/screens/DosesScreen.kt +++ b/app/src/main/java/com/hormonetrack/ui/screens/DosesScreen.kt @@ -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 = { diff --git a/app/src/main/java/com/hormonetrack/ui/screens/HomeScreen.kt b/app/src/main/java/com/hormonetrack/ui/screens/HomeScreen.kt index e19e401..f322056 100644 --- a/app/src/main/java/com/hormonetrack/ui/screens/HomeScreen.kt +++ b/app/src/main/java/com/hormonetrack/ui/screens/HomeScreen.kt @@ -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( - 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 }, diff --git a/app/src/main/java/com/hormonetrack/ui/screens/TreatmentEditorScreen.kt b/app/src/main/java/com/hormonetrack/ui/screens/TreatmentEditorScreen.kt index e9ea5e1..cc677d9 100644 --- a/app/src/main/java/com/hormonetrack/ui/screens/TreatmentEditorScreen.kt +++ b/app/src/main/java/com/hormonetrack/ui/screens/TreatmentEditorScreen.kt @@ -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) diff --git a/app/src/test/java/com/hormonetrack/pk/RegressionUserCase3Test.kt b/app/src/test/java/com/hormonetrack/pk/RegressionUserCase3Test.kt new file mode 100644 index 0000000..7d8d20f --- /dev/null +++ b/app/src/test/java/com/hormonetrack/pk/RegressionUserCase3Test.kt @@ -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) + } + } +} diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 3db6785..75991de 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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) diff --git a/docs/DEVELOPPEMENT.md b/docs/DEVELOPPEMENT.md index 249e555..1120d41 100644 --- a/docs/DEVELOPPEMENT.md +++ b/docs/DEVELOPPEMENT.md @@ -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.* diff --git a/docs/GUIDE_INSTALLATION.md b/docs/GUIDE_INSTALLATION.md index 4976fb6..c5bb203 100644 --- a/docs/GUIDE_INSTALLATION.md +++ b/docs/GUIDE_INSTALLATION.md @@ -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)