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:
parent
9301b96902
commit
3f9b2bd8a0
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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 },
|
||||
|
||||
@ -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)
|
||||
|
||||
146
app/src/test/java/com/hormonetrack/pk/RegressionUserCase3Test.kt
Normal file
146
app/src/test/java/com/hormonetrack/pk/RegressionUserCase3Test.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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.*
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user