v1.4.3 : fix #53 — le delta 'vs il y a 6 h' de l'accueil comparait en réalité il y a 24 h (firstOrNull sur la fenêtre 24 h) → pointHoursBefore (point au plus petit écart ≥ 6 h) + 4 tests ; régression n°4 épinglée sur le 4ᵉ export réel (contribution EEn J+1 < 12 %, plateau d'équilibre — comportement pharmacocinétique normal confirmé avec le vrai moteur) ; 137 tests verts + lint
This commit is contained in:
parent
633c6debf4
commit
a6ed2da96a
@ -16,7 +16,7 @@ sauvegarde JSON. **100 % local, aucun compte, aucun serveur.**
|
|||||||
> à titre informatif — ce ne sont pas des mesures. Fie-toi toujours à tes analyses de
|
> à titre informatif — ce ne sont pas des mesures. Fie-toi toujours à tes analyses de
|
||||||
> sang et aux consignes de ton endocrinologue.
|
> sang et aux consignes de ton endocrinologue.
|
||||||
|
|
||||||
- **Statut** : v1.4.2 — build Android ✅, **lint vert** ✅, **129 tests unitaires** ✅ (111 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** ✅
|
- **Statut** : v1.4.3 — build Android ✅, **lint vert** ✅, **137 tests unitaires** ✅ (115 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)
|
- **Journal des versions** : [docs/CHANGELOG.md](docs/CHANGELOG.md)
|
||||||
- **Guide utilisateur** : [docs/GUIDE_INSTALLATION.md](docs/GUIDE_INSTALLATION.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)
|
- **Doc de développement** (architecture, maths, décisions, bugs) : [docs/DEVELOPPEMENT.md](docs/DEVELOPPEMENT.md)
|
||||||
@ -88,7 +88,7 @@ par AGP si les licences sont signées). Le wrapper télécharge Gradle 9.7.1.
|
|||||||
git clone <repo> && cd HormoneTrack
|
git clone <repo> && cd HormoneTrack
|
||||||
echo "sdk.dir=/chemin/vers/android-sdk" > local.properties # ou ANDROID_HOME
|
echo "sdk.dir=/chemin/vers/android-sdk" > local.properties # ou ANDROID_HOME
|
||||||
./gradlew assembleDebug # APK : app/build/outputs/apk/debug/app-debug.apk
|
./gradlew assembleDebug # APK : app/build/outputs/apk/debug/app-debug.apk
|
||||||
./gradlew testDebugUnitTest # 129 tests (111 sans les données locales)
|
./gradlew testDebugUnitTest # 137 tests (115 sans les données locales)
|
||||||
./gradlew lint # lint vert obligatoire avant release
|
./gradlew lint # lint vert obligatoire avant release
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -100,7 +100,7 @@ Pas de Play Store : l'app est sideloadée. Détails pas-à-pas : [docs/GUIDE_INS
|
|||||||
|
|
||||||
Dépôts : **gitea.cloudyfy.fr** et **gitea.farewell.dev** (miroir) —
|
Dépôts : **gitea.cloudyfy.fr** et **gitea.farewell.dev** (miroir) —
|
||||||
`Siphonight/HormoneTrack` sur les deux (privé), avec
|
`Siphonight/HormoneTrack` sur les deux (privé), avec
|
||||||
**releases taguées** (`v1.1.0` → `v1.4.2`) et **deux APK par release** (depuis v1.2.5)
|
**releases taguées** (`v1.1.0` → `v1.4.3`) et **deux APK par release** (depuis v1.2.5)
|
||||||
(téléchargeables sans compiler, cf [docs/DEVELOPPEMENT.md §16.bis](docs/DEVELOPPEMENT.md)) :
|
(téléchargeables sans compiler, cf [docs/DEVELOPPEMENT.md §16.bis](docs/DEVELOPPEMENT.md)) :
|
||||||
`-release.apk` (**recommandé**, optimisé R8, 2,4 Mo) et `-debug.apk` (20 Mo) :
|
`-release.apk` (**recommandé**, optimisé R8, 2,4 Mo) et `-debug.apk` (20 Mo) :
|
||||||
|
|
||||||
|
|||||||
@ -17,8 +17,8 @@ android {
|
|||||||
// tag : v1.3.0→v1.3.2 contenaient tous versionCode 14 / "1.3.0"
|
// tag : v1.3.0→v1.3.2 contenaient tous versionCode 14 / "1.3.0"
|
||||||
// (bump jamais commité) → BuildConfig.VERSION_NAME était faux dans
|
// (bump jamais commité) → BuildConfig.VERSION_NAME était faux dans
|
||||||
// les APK publiés (Paramètres + titre du dialog « Nouveautés »).
|
// les APK publiés (Paramètres + titre du dialog « Nouveautés »).
|
||||||
versionCode = 22
|
versionCode = 23
|
||||||
versionName = "1.4.2"
|
versionName = "1.4.3"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|||||||
@ -100,6 +100,33 @@ fun forecastHorizonHours(maxIntervalDays: Double?): Long? {
|
|||||||
fun clampPanHours(panHours: Long, futureHorizonHours: Long, maxPastHours: Long): Long =
|
fun clampPanHours(panHours: Long, futureHorizonHours: Long, maxPastHours: Long): Long =
|
||||||
panHours.coerceIn(-futureHorizonHours.coerceAtLeast(0L), maxPastHours.coerceAtLeast(0L))
|
panHours.coerceIn(-futureHorizonHours.coerceAtLeast(0L), maxPastHours.coerceAtLeast(0L))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Point le plus PROCHE de `hours` heures AVANT le dernier point de la série
|
||||||
|
* (v1.4.3, fix #53) : utilisé par le delta « vs il y a 6 h » de la carte
|
||||||
|
* d'accueil.
|
||||||
|
*
|
||||||
|
* ⚠️ BUG corrigé ici : l'ancien code faisait `curve.firstOrNull { écart >= 6 h }`
|
||||||
|
* sur une fenêtre de 24 h → le PREMIER point de la liste (il y a ~24 h !)
|
||||||
|
* satisfaisait la condition immédiatement, et le delta affiché « vs 6 h »
|
||||||
|
* comparait en réalité à il y a 24 h (remontée : « −11 pg/mL » le lendemain
|
||||||
|
* d'une injection EEn, cf régression #4). Cette fonction prend le point dont
|
||||||
|
* l'écart au dernier est le plus PETIT au-dessus de `hours` (par excès —
|
||||||
|
* avec une grille régulière, c'est le point à ~6 h exactement).
|
||||||
|
*
|
||||||
|
* @return null si aucun point n'est au-delà de `hours` (courbe trop courte).
|
||||||
|
*/
|
||||||
|
fun pointHoursBefore(points: List<LevelPoint>, hours: Double): LevelPoint? {
|
||||||
|
val last = points.lastOrNull() ?: return null
|
||||||
|
var best: LevelPoint? = null
|
||||||
|
var bestGap = Double.MAX_VALUE
|
||||||
|
for (p in points) {
|
||||||
|
val gap = (last.timestamp - p.timestamp) / 3_600_000.0
|
||||||
|
if (gap < hours) continue // seulement AVANT le point de référence
|
||||||
|
if (gap < bestGap) { bestGap = gap; best = p }
|
||||||
|
}
|
||||||
|
return best
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Graphique Canvas pur, multi-séries :
|
* Graphique Canvas pur, multi-séries :
|
||||||
* - axe gauche = E2 (pg/mL), axe droit = T (ng/mL) ;
|
* - axe gauche = E2 (pg/mL), axe droit = T (ng/mL) ;
|
||||||
|
|||||||
@ -346,9 +346,11 @@ private fun List<LabResult>.filterInRange(curve: List<LevelPoint>, marker: Strin
|
|||||||
@Composable
|
@Composable
|
||||||
private fun NowLevelCard(curve: List<LevelPoint>) {
|
private fun NowLevelCard(curve: List<LevelPoint>) {
|
||||||
val now = curve.lastOrNull()
|
val now = curve.lastOrNull()
|
||||||
val sixHAgo = curve.firstOrNull { p ->
|
// v1.4.3 (fix #53) : le point de comparaison « il y a 6 h » est celui le
|
||||||
(curve.last().timestamp - p.timestamp) >= 6 * HOUR_MS
|
// plus PROCHE de −6 h (l'ancien code prenait le PREMIER point de la
|
||||||
}
|
// fenêtre 24 h → le delta affiché était en réalité un delta 24 h,
|
||||||
|
// trompeur le lendemain d'une injection lente type EEn — cf régression #4).
|
||||||
|
val sixHAgo = com.hormonetrack.ui.components.pointHoursBefore(curve, hours = 6.0)
|
||||||
Card(
|
Card(
|
||||||
Modifier.fillMaxWidth(),
|
Modifier.fillMaxWidth(),
|
||||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
|
||||||
|
|||||||
154
app/src/test/java/com/hormonetrack/pk/RegressionUserCase4Test.kt
Normal file
154
app/src/test/java/com/hormonetrack/pk/RegressionUserCase4Test.kt
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* **4ᵉ régression épinglée sur données réelles** (export v1.4.2, HORS dépôt
|
||||||
|
* `local-test-data/backup-v1.4.2.json`, gitignoré — données de santé).
|
||||||
|
*
|
||||||
|
* Contexte (remontée 2026-09-07) : l'utilisatrice s'est injectée de l'EEn
|
||||||
|
* la veille à ~18:54 et trouve « bizarre » que l'app affiche un taux qui
|
||||||
|
* DESCEND (238 pg/mL, −11 « vs il y a 6 h »). Deux enseignements épinglés :
|
||||||
|
*
|
||||||
|
* 1. **Pharmacocinétique normale** : à J+1 d'un EEn (Tmax 6,5 j), la dose
|
||||||
|
* ne contribue encore qu'à peine (~4 pg/mL à J+0,5, ~18 à J+1) et le
|
||||||
|
* niveau TOTAL est un plateau (superposition de l'équilibre — PTR bas de
|
||||||
|
* l'énanthate). La « remontée » individuelle est masquée par la
|
||||||
|
* décroissance du stock. Ce test épine cette signature.
|
||||||
|
* 2. **Bug #53 confirmé dans le code** : le delta « vs il y a 6 h » de la
|
||||||
|
* carte d'accueil comparait en réalité le point d'IL Y A ~24 H
|
||||||
|
* (`firstOrNull` sur la fenêtre 24 h) — cf fix `pointHoursBefore`.
|
||||||
|
*
|
||||||
|
* Le test reproduit le calcul d'app à l'instant d'observation (+15 min
|
||||||
|
* après exportedAt ≈ 07:25 le lendemain de l'injection) pour épingler le
|
||||||
|
* niveau exact que le moteur produit.
|
||||||
|
*/
|
||||||
|
class RegressionUserCase4Test {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DATA_FILE: File = listOf(
|
||||||
|
File("../local-test-data/backup-v1.4.2.json"),
|
||||||
|
File("local-test-data/backup-v1.4.2.json"),
|
||||||
|
File("app/local-test-data/backup-v1.4.2.json")
|
||||||
|
).firstOrNull { it.exists() } ?: File("../local-test-data/backup-v1.4.2.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var data: BackupData
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
assumeTrue(
|
||||||
|
"local-test-data/backup-v1.4.2.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())
|
||||||
|
}
|
||||||
|
data = Gson().fromJson(DATA_FILE.readText(), BackupData::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observationMs(): Long = data.exportedAt + 15 * 60_000L
|
||||||
|
|
||||||
|
/** Calibration automatique telle que la calcule l'app (option active). */
|
||||||
|
private fun auto(): PharmacokineticEngine.AutoCalibrated =
|
||||||
|
PharmacokineticEngine.autoCalibrated(
|
||||||
|
data.treatments, data.doseLogs, data.labResults, data.tConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `export v2 parses with settings and the real transition scenario`() {
|
||||||
|
assertEquals(3, data.treatments.size)
|
||||||
|
assertEquals(52, data.doseLogs.size)
|
||||||
|
assertEquals(26, data.labResults.size)
|
||||||
|
assertEquals(2, data.version)
|
||||||
|
// settings v1.4.2 : opt-in alertes + auto-calibration active
|
||||||
|
org.junit.Assert.assertNotNull(data.settings)
|
||||||
|
assertEquals(true, data.settings!!.autoCalibrate)
|
||||||
|
org.junit.Assert.assertNotNull(data.settings!!.alertE2High)
|
||||||
|
org.junit.Assert.assertNotNull(data.settings!!.alertE2Low)
|
||||||
|
// scénario de transition : EV inactif (scaleFactor > 1) + EEn actif (posologie 6 j)
|
||||||
|
val ev = data.treatments.single { it.esterType == "EV" }
|
||||||
|
val een = data.treatments.single { it.esterType == "EEN" }
|
||||||
|
assertTrue(!ev.isActive && ev.scaleFactor != 1.0)
|
||||||
|
assertTrue(een.isActive && een.forecastIntervalDays != null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `contribution of yesterday injection is tiny at J+1 (EEn Tmax 6-5 days)`() {
|
||||||
|
val eenDoses = data.doseLogs.filter { it.treatmentId == 1L }
|
||||||
|
val lastDose = eenDoses.maxByOrNull { it.timestamp }!!
|
||||||
|
val obs = observationMs()
|
||||||
|
val dtDays = (obs - lastDose.timestamp) / 86_400_000.0
|
||||||
|
assertTrue("l'observation est ~J+0,5", dtDays in 0.3..0.7)
|
||||||
|
|
||||||
|
// contribution SEULE de la dernière dose, en % du niveau total
|
||||||
|
val tr = data.treatments.single { it.id == lastDose.treatmentId }
|
||||||
|
val alone = PharmacokineticEngine.concentrationOfDose(tr, lastDose, obs)
|
||||||
|
val total = PharmacokineticEngine.e2At(
|
||||||
|
data.treatments, data.doseLogs, obs, scalePerEster = auto().esterScales
|
||||||
|
)
|
||||||
|
assertTrue("contribution seule=${alone} pg/mL doit être faible (< 12 %)", alone < total * 0.12)
|
||||||
|
// signature EEn : à J+1 la contribution reste marginale devant le plateau
|
||||||
|
val aloneJ1 = PharmacokineticEngine.concentrationOfDose(
|
||||||
|
tr, lastDose, lastDose.timestamp + 86_400_000L
|
||||||
|
)
|
||||||
|
val totalJ1 = PharmacokineticEngine.e2At(
|
||||||
|
data.treatments, data.doseLogs, lastDose.timestamp + 86_400_000L,
|
||||||
|
scalePerEster = auto().esterScales
|
||||||
|
)
|
||||||
|
assertTrue("à J+1 : $aloneJ1 vs total ${totalJ1}", aloneJ1 < totalJ1 * 0.12)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `level plateau around observation time (EEn equilibrium, no sharp rise)`() {
|
||||||
|
val obs = observationMs()
|
||||||
|
val scale = auto().esterScales
|
||||||
|
// Variation sur 24 h autour de l'observation : le plateau EEn est plat
|
||||||
|
// (PTR bas) — |Δ| reste une petite fraction du niveau.
|
||||||
|
val before = PharmacokineticEngine.e2At(
|
||||||
|
data.treatments, data.doseLogs, obs - 86_400_000L, scalePerEster = scale
|
||||||
|
)
|
||||||
|
val at = PharmacokineticEngine.e2At(
|
||||||
|
data.treatments, data.doseLogs, obs, scalePerEster = scale
|
||||||
|
)
|
||||||
|
val after = PharmacokineticEngine.e2At(
|
||||||
|
data.treatments, data.doseLogs, obs + 86_400_000L, scalePerEster = scale
|
||||||
|
)
|
||||||
|
assertTrue("niveau=${at} doit être physiologique (100-500)", at in 100.0..500.0)
|
||||||
|
val maxSwing = maxOf(before, at, after) - minOf(before, at, after)
|
||||||
|
assertTrue("swing 48 h = ${maxSwing} doit rester ≤ 25 % du niveau", maxSwing <= at * 0.25)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `thresholds evaluate on the real level (no false HIGH at observation)`() {
|
||||||
|
val obs = observationMs()
|
||||||
|
val scale = auto().esterScales
|
||||||
|
val e2 = PharmacokineticEngine.e2At(data.treatments, data.doseLogs, obs, scalePerEster = scale)
|
||||||
|
val t = data.tConfig
|
||||||
|
val level = PharmacokineticEngine.levelAt(
|
||||||
|
data.treatments, data.doseLogs, obs, t,
|
||||||
|
tKPerEster = auto().tKPerEster, scalePerEster = scale
|
||||||
|
)
|
||||||
|
val alerts = Alerts.evaluateAll(level.e2, level.t, Alerts.Thresholds(
|
||||||
|
e2High = data.settings!!.alertE2High,
|
||||||
|
e2Low = data.settings!!.alertE2Low,
|
||||||
|
tHigh = data.settings!!.alertTHigh,
|
||||||
|
tLow = data.settings!!.alertTLow
|
||||||
|
))
|
||||||
|
// À l'observation (J+0,5) : le niveau est dans la plage configurée →
|
||||||
|
// PAS d'alerte (le franchissement LOW n'arrive qu'au creux, ~J+7)
|
||||||
|
assertTrue("alertes inattendues à l'observation : ${alerts}", alerts.isEmpty())
|
||||||
|
assertTrue("niveau=$e2 cohérent avec l'affichage rapporté (~238)", e2 in 200.0..300.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -76,4 +76,47 @@ class ChartZoomTest {
|
|||||||
// bornes dégénérées (jamais négatives)
|
// bornes dégénérées (jamais négatives)
|
||||||
assertEquals(0L, clampPanHours(-10L, -5L, -3L))
|
assertEquals(0L, clampPanHours(-10L, -5L, -3L))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── v1.4.3 (fix #53) : delta « vs il y a 6 h » de la carte d'accueil ──
|
||||||
|
|
||||||
|
private fun point(tsH: Long, e2: Double = 0.0) =
|
||||||
|
com.hormonetrack.pk.LevelPoint(timestamp = tsH * 3_600_000L, e2 = e2, t = 0.0)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `pointHoursBefore picks the point closest to 6h before the last one`() {
|
||||||
|
// Fenêtre 24 h à pas d'1 h : le point à 24 − 6 = 18 h exactement
|
||||||
|
val hourly = (0L..24L).map { point(it) }
|
||||||
|
val chosen = com.hormonetrack.ui.components.pointHoursBefore(hourly, 6.0)!!
|
||||||
|
assertEquals(18L * 3_600_000L, chosen.timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `pointHoursBefore on half-hour grid lands exactly 6h back`() {
|
||||||
|
val halfHourly = (0L..48L).map {
|
||||||
|
com.hormonetrack.pk.LevelPoint(timestamp = it * 1_800_000L, e2 = 0.0, t = 0.0)
|
||||||
|
}
|
||||||
|
val chosen = com.hormonetrack.ui.components.pointHoursBefore(halfHourly, 6.0)!!
|
||||||
|
val gapH = (halfHourly.last().timestamp - chosen.timestamp) / 3_600_000.0
|
||||||
|
assertEquals(6.0, gapH, 1e-9)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `pointHoursBefore returns null when the curve is shorter than the offset`() {
|
||||||
|
val short = (0L..5L).map { point(it) } // 5 h de couverture
|
||||||
|
assertNull(com.hormonetrack.ui.components.pointHoursBefore(short, 6.0))
|
||||||
|
assertNull(com.hormonetrack.ui.components.pointHoursBefore(emptyList(), 6.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `pointHoursBefore with irregular points takes the closest above the offset`() {
|
||||||
|
// points : 0h, 7h, 23h → le point le plus proche de 6h (par excès) = 7h
|
||||||
|
val irregular = listOf(point(0), point(7), point(23))
|
||||||
|
val chosen = com.hormonetrack.ui.components.pointHoursBefore(irregular, 6.0)!!
|
||||||
|
assertEquals(7L * 3_600_000L, chosen.timestamp)
|
||||||
|
// points : 0h, 3h, 23h → les deux sont au-delà de 6h ; on garde le
|
||||||
|
// gap le PLUS PETIT (3h, gap 20 h) et non le plus ancien
|
||||||
|
val last = listOf(point(0), point(3), point(23))
|
||||||
|
val chosen2 = com.hormonetrack.ui.components.pointHoursBefore(last, 6.0)!!
|
||||||
|
assertEquals(3L * 3_600_000L, chosen2.timestamp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,33 @@
|
|||||||
# Changelog — HormoneTrack
|
# Changelog — HormoneTrack
|
||||||
|
|
||||||
Format : [Keep a Changelog](https://keepachangelog.com/fr-FR/1.1.0/).
|
Format : [Keep a Changelog](https://keepachangelog.com/fr-FR/1.1.0/).
|
||||||
Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.4.2`, …).
|
Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.4.3`, …).
|
||||||
|
|
||||||
|
## [1.4.3] — 2026-09-07 (versionCode 23)
|
||||||
|
|
||||||
|
### Corrigé
|
||||||
|
- **Le delta « vs il y a 6 h » de la carte d'accueil comparait en réalité
|
||||||
|
il y a ~24 h** (#53, remontée : « 238 pg/mL avec −11 vs 6 h malgré mon
|
||||||
|
injection d'hier ») : `curve.firstOrNull { écart ≥ 6 h }` sur une fenêtre
|
||||||
|
de 24 h retournait le PREMIER point de la liste (il y a 24 h), pas un
|
||||||
|
point à 6 h. FIX : nouveau helper pur **`pointHoursBefore`** (le point
|
||||||
|
dont l'écart au dernier est le plus petit au-dessus de l'offset —
|
||||||
|
testé : grille 1 h, grille 30 min, points irréguliers, courbe courte).
|
||||||
|
Le delta affiché est désormais VRAIMENT un delta 6 h.
|
||||||
|
|
||||||
|
### Ajouté
|
||||||
|
- **Régression n°4** épinglée sur le 4ᵉ export réel (HORS dépôt, gitignoré)
|
||||||
|
: le scénario « injection EEn hier, l'estimation ne remonte pas » —
|
||||||
|
export v1.4.2 (3 traitements, 52 doses, 26 labs, settings v2 avec seuils
|
||||||
|
et auto-calibration). Le test épine, avec le VRAI moteur : la
|
||||||
|
**contribution d'une dose EEn à J+1 est marginale** (< 12 % du total —
|
||||||
|
Tmax 6,5 j), le **plateau d'équilibre** (swing 48 h ≤ 25 % du niveau),
|
||||||
|
l'absence de fausse alerte à l'observation, et le parsing des settings
|
||||||
|
v2. Réponse à la question posée : **comportement pharmacocinétique
|
||||||
|
normal** de l'énanthate (courbe plate en équilibre), pas un bug — le
|
||||||
|
seul bug était l'étiquette du delta (ci-dessus).
|
||||||
|
- **8 nouveaux tests** (137 au total / 115 sans les données locales) :
|
||||||
|
`RegressionUserCase4Test` (4) + `pointHoursBefore` (4, dans ChartZoomTest).
|
||||||
|
|
||||||
## [1.4.2] — 2026-09-07 (versionCode 22)
|
## [1.4.2] — 2026-09-07 (versionCode 22)
|
||||||
|
|
||||||
|
|||||||
@ -85,6 +85,7 @@ Fonctionnalités v1 :
|
|||||||
| 6 sept. 2026 (session v1.4.0) | **Modèle TFS reconstruit sur la méta-analyse officielle** (params V3C du simulateur transfemscience.github.io/injectable-e2-simulator, extraits de `ester-data.js`/`calc-curve.js`) : forme close 3 compartiments, 7 esters (EB/EC/ECS/PEP ajoutés), pics de l'article reproduits à ~1 % (AUC + Figure 11 épinglés). **+ Fix #52** (remontée) : rappels quotidiens même hors jour d'injection → grille Posologie (moteur = source unique), re-programmation après notif et « Pris ». 12 nouveaux tests (102 verts), lint vert. Publication v1.4.0. |
|
| 6 sept. 2026 (session v1.4.0) | **Modèle TFS reconstruit sur la méta-analyse officielle** (params V3C du simulateur transfemscience.github.io/injectable-e2-simulator, extraits de `ester-data.js`/`calc-curve.js`) : forme close 3 compartiments, 7 esters (EB/EC/ECS/PEP ajoutés), pics de l'article reproduits à ~1 % (AUC + Figure 11 épinglés). **+ Fix #52** (remontée) : rappels quotidiens même hors jour d'injection → grille Posologie (moteur = source unique), re-programmation après notif et « Pris ». 12 nouveaux tests (102 verts), lint vert. Publication v1.4.0. |
|
||||||
| 6 sept. 2026 (session v1.4.1) | **UX graphique et carte « Prochaine dose »** (remontées) : (a) activer la Prévision ne saute PLUS dans le futur — la fenêtre reste en place, la projection s'étend au-delà et se parcourt en tirant vers la gauche (panHours NÉGATIF = futur, clamp unique `clampPanHours`, horizon 12× Posologie borné 30 j–1 an) ; (b) delta en JOURS sur la carte « Prochaine dose » au-delà de 24 h (`HrtDuration.daysAndHours` + `next_dose_days` FR/EN). 5 tests (112 verts). Publication v1.4.1. |
|
| 6 sept. 2026 (session v1.4.1) | **UX graphique et carte « Prochaine dose »** (remontées) : (a) activer la Prévision ne saute PLUS dans le futur — la fenêtre reste en place, la projection s'étend au-delà et se parcourt en tirant vers la gauche (panHours NÉGATIF = futur, clamp unique `clampPanHours`, horizon 12× Posologie borné 30 j–1 an) ; (b) delta en JOURS sur la carte « Prochaine dose » au-delà de 24 h (`HrtDuration.daysAndHours` + `next_dose_days` FR/EN). 5 tests (112 verts). Publication v1.4.1. |
|
||||||
| 6 sept. 2026 (session v1.4.2) | **Seuils d'alerte configurables + notification** (demande) : limites E2/T hautes/basses (Paramètres, opt-in, validées haut > bas), cartes d'avertissement sur l'accueil (niveau ESTIMÉ actuel), **notification** via un worker WorkManager périodique 15 min (enfin utilisé !) + check one-time au save, anti-spam par état persisté (`Alerts.encodeState`/`shouldNotify`), canal dédié `hormonetrack_alerts`. + **backup JSON v2** : les PARAMÈTRES utilisateur (langue, auto-cal, seuils) voyagent dans l'export et sont restaurés à l'import (rétrocompat v1, `-keep` R8 pour UserSettings). 9 tests (129 verts). Publication v1.4.2. |
|
| 6 sept. 2026 (session v1.4.2) | **Seuils d'alerte configurables + notification** (demande) : limites E2/T hautes/basses (Paramètres, opt-in, validées haut > bas), cartes d'avertissement sur l'accueil (niveau ESTIMÉ actuel), **notification** via un worker WorkManager périodique 15 min (enfin utilisé !) + check one-time au save, anti-spam par état persisté (`Alerts.encodeState`/`shouldNotify`), canal dédié `hormonetrack_alerts`. + **backup JSON v2** : les PARAMÈTRES utilisateur (langue, auto-cal, seuils) voyagent dans l'export et sont restaurés à l'import (rétrocompat v1, `-keep` R8 pour UserSettings). 9 tests (129 verts). Publication v1.4.2. |
|
||||||
|
| 7 sept. 2026 (session v1.4.3) | **Question pharmacocinétique → 1 bug + 1 confirmation** : « l'estimation ne remonte pas le lendemain de mon injection EEn » → (a) **comportement NORMAL** (contribution J+1 < 12 % du total, plateau d'équilibre — épinglé par la régression n°4 sur le 4ᵉ export réel) ; (b) **bug #53** : le delta « vs il y a 6 h » de l'accueil comparait en réalité il y a 24 h (`firstOrNull` sur la fenêtre 24 h) → helper `pointHoursBefore` + 4 tests. 137 verts. Publication v1.4.3. |
|
||||||
| 6 sept. 2026 (session v1.3.0) | Dialog « Nouveautés » après mise à jour (CHANGELOG embarqué en asset, tâche `copyChangelog`, version vue en DataStore) ; **événements d'agenda récurrents** (calendrier local HormoneTrack, RRULE posologie, permission runtime, Room v3 `calendarEventId`) ; Paramètres : version + lien releases ; **78 tests verts**, APK v1.3.0 + releases. |
|
| 6 sept. 2026 (session v1.3.0) | Dialog « Nouveautés » après mise à jour (CHANGELOG embarqué en asset, tâche `copyChangelog`, version vue en DataStore) ; **événements d'agenda récurrents** (calendrier local HormoneTrack, RRULE posologie, permission runtime, Room v3 `calendarEventId`) ; Paramètres : version + lien releases ; **78 tests verts**, APK v1.3.0 + releases. |
|
||||||
| 5 sept. 2026 (session v1.2.10) | Sens des boutons de zoom inversé (+ = zoom avant, convention carte — retour utilisateur) ; 67 tests verts, APK v1.2.10 + releases. |
|
| 5 sept. 2026 (session v1.2.10) | Sens des boutons de zoom inversé (+ = zoom avant, convention carte — retour utilisateur) ; 67 tests verts, APK v1.2.10 + releases. |
|
||||||
| 5 sept. 2026 (session v1.2.9) | **Zoom du graphique** (pinch + boutons, 6 h → 300 j, focal stable, échantillonnage adaptatif `stepForRange`, labels X 1 h/3 h) ; **README : disclaimer IA-assisté** en en-tête ; 3 tests ; **67 tests verts**, APK v1.2.9 + releases. |
|
| 5 sept. 2026 (session v1.2.9) | **Zoom du graphique** (pinch + boutons, 6 h → 300 j, focal stable, échantillonnage adaptatif `stepForRange`, labels X 1 h/3 h) ; **README : disclaimer IA-assisté** en en-tête ; 3 tests ; **67 tests verts**, APK v1.2.9 + releases. |
|
||||||
@ -409,7 +410,7 @@ Cp(t) = D·k1·k2 · [ e^(−k1·t)/((k1−k2)(k1−k3)) + e^(−k3·t)/((k1−k
|
|||||||
|
|
||||||
## 8. Tests unitaires
|
## 8. Tests unitaires
|
||||||
|
|
||||||
**129 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — **111 sans
|
**137 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — **115 sans
|
||||||
les données de test locales** (cf §8.bis : les 3 classes de régression,
|
les données de test locales** (cf §8.bis : les 3 classes de régression,
|
||||||
6 tests chacune, sont skippées via `Assume`). Dépendance : JUnit 4.13.2.
|
6 tests chacune, sont skippées via `Assume`). Dépendance : JUnit 4.13.2.
|
||||||
Emplacement : `app/src/test/java/com/hormonetrack/`. Répertoire de travail d'exécution =
|
Emplacement : `app/src/test/java/com/hormonetrack/`. Répertoire de travail d'exécution =
|
||||||
@ -520,6 +521,13 @@ dépôt (ni maintenant, ni si le repo devient public un jour).
|
|||||||
d'équilibre EV 5 mg/7 j = Figure 11 (Cmax 384/Cmin 142/Cavg 269), esters
|
d'équilibre EV 5 mg/7 j = Figure 11 (Cmax 384/Cmin 142/Cavg 269), esters
|
||||||
sans modèle → 0, dt ≤ 0 → 0. **Toute retouche de TransfemScienceModels.kt
|
sans modèle → 0, dt ≤ 0 → 0. **Toute retouche de TransfemScienceModels.kt
|
||||||
passe par CE test** (c'est la fidélité au simulateur TFS qui est vérifiée).
|
passe par CE test** (c'est la fidélité au simulateur TFS qui est vérifiée).
|
||||||
|
- **`RegressionUserCase4Test`** (4) : v1.4.3 — 4ᵉ régression épinglée sur
|
||||||
|
les données réelles (export v1.4.2, HORS dépôt) : parsing v2 avec
|
||||||
|
**settings** (seuils + auto-cal), scénario transition EV inactif → EEn
|
||||||
|
actif, **signature pharmacocinétique EEn** (contribution d'une dose à
|
||||||
|
J+1 < 12 % du total, plateau 48 h ≤ 25 % — c'est la réponse épinglée à
|
||||||
|
« pourquoi l'estimation ne remonte pas le lendemain »), pas de fausse
|
||||||
|
alerte à l'observation. **Tout nouvel export = une régression data-driven.**
|
||||||
- **`AlertsTest`** (12) : v1.4.2 — logique pure des seuils (`pk/Alerts.kt`) :
|
- **`AlertsTest`** (12) : v1.4.2 — logique pure des seuils (`pk/Alerts.kt`) :
|
||||||
HIGH/LOW, comparaison STRICTE (valeur == limite → rien, éviter le
|
HIGH/LOW, comparaison STRICTE (valeur == limite → rien, éviter le
|
||||||
clignotement d'arrondi), seuil null → jamais d'alerte, cohérence haut > bas,
|
clignotement d'arrondi), seuil null → jamais d'alerte, cohérence haut > bas,
|
||||||
@ -627,7 +635,9 @@ Sur la montre : remontée par Gadgetbridge **ou** Huawei Health (cf §17).
|
|||||||
- `HormoneTrackRoot` : NavigationBar 5 tabs (home/chart/doses/labs/treatments) + routes
|
- `HormoneTrackRoot` : NavigationBar 5 tabs (home/chart/doses/labs/treatments) + routes
|
||||||
`settings`, `treatment_edit/{id}` (-1 = nouveau) ; barre masquée sur ces 2 routes
|
`settings`, `treatment_edit/{id}` (-1 = nouveau) ; barre masquée sur ces 2 routes
|
||||||
- `HomeScreen` : bandeau gradient (TransSky→TransPink, discret), carte **niveau actuel**
|
- `HomeScreen` : bandeau gradient (TransSky→TransPink, discret), carte **niveau actuel**
|
||||||
(E2 ≈ X pg/mL, T ≈ Y ng/mL, delta vs 6 h), **cartes d'alerte v1.4.2** (si un
|
(E2 ≈ X pg/mL, T ≈ Y ng/mL, delta vs 6 h — **v1.4.3 : comparaison au point le
|
||||||
|
plus proche de −6 h via `pointHoursBefore`, l'ancien code comparait ~24 h**,
|
||||||
|
bug #53), **cartes d'alerte v1.4.2** (si un
|
||||||
seuil est franchi : fond `errorContainer`, « ▲ E2 estimé ≈ X pg/mL — au-dessus
|
seuil est franchi : fond `errorContainer`, « ▲ E2 estimé ≈ X pg/mL — au-dessus
|
||||||
de ta limite (Y) », basées sur le DERNIER point de courbe = niveau affiché,
|
de ta limite (Y) », basées sur le DERNIER point de courbe = niveau affiché,
|
||||||
disclaimer « estimation, pas une mesure »), carte prochaine dose (**v1.4.1 :
|
disclaimer « estimation, pas une mesure »), carte prochaine dose (**v1.4.1 :
|
||||||
@ -1049,6 +1059,16 @@ l'exemple) :**
|
|||||||
|
|
||||||
**Session v1.4.0 (modèle TFS sur la méta-analyse + fix #52) :**
|
**Session v1.4.0 (modèle TFS sur la méta-analyse + fix #52) :**
|
||||||
|
|
||||||
|
53. **Delta « vs il y a 6 h » = en réalité 24 h** (v1.4.3, remontée) :
|
||||||
|
`NowLevelCard` prenait `curve.firstOrNull { écart ≥ 6 h }` sur une
|
||||||
|
fenêtre de 24 h — le premier point satisfait la condition
|
||||||
|
immédiatement → comparaison à ~24 h. Le lendemain d'une injection EEn
|
||||||
|
(plateau), ce delta 24 h peut être légèrement négatif → fausse
|
||||||
|
impression que « l'injection ne fait rien ». FIX : `pointHoursBefore`
|
||||||
|
(point au plus petit écart ≥ 6 h, testé). Leçon : un `firstOrNull` sur
|
||||||
|
une condition de distance trouve l'élément le PLUS LOIN, jamais le plus
|
||||||
|
proche — écrire l'intention (« le plus proche de N ») explicitement.
|
||||||
|
|
||||||
52. **Rappels quotidiens même hors jour d'injection** (remontée utilisateur :
|
52. **Rappels quotidiens même hors jour d'injection** (remontée utilisateur :
|
||||||
« je m'injecte tous les samedis à 18 h → je dois recevoir le rappel
|
« je m'injecte tous les samedis à 18 h → je dois recevoir le rappel
|
||||||
uniquement le samedi à 18 h ») : `nextReminderFireMs` ignorait doses et
|
uniquement le samedi à 18 h ») : `nextReminderFireMs` ignorait doses et
|
||||||
@ -1208,8 +1228,8 @@ vérifie pas en tests JVM).
|
|||||||
|
|
||||||
1. **Bumper la version** dans `app/build.gradle.kts` : `versionCode = N+1`,
|
1. **Bumper la version** dans `app/build.gradle.kts` : `versionCode = N+1`,
|
||||||
`versionName = "X.Y.Z+1"` (SemVer : fix = Z, feature = Y).
|
`versionName = "X.Y.Z+1"` (SemVer : fix = Z, feature = Y).
|
||||||
2. **Tests verts obligatoires** : `./gradlew testDebugUnitTest` — 129 au
|
2. **Tests verts obligatoires** : `./gradlew testDebugUnitTest` — 137 au
|
||||||
total, 111 si `local-test-data/` est absent (les 3 classes de régression
|
total, 115 si `local-test-data/` est absent (les 3 classes de régression
|
||||||
réelles sont skippées via `Assume`, 6 tests chacune) ; **lint vert
|
réelles sont skippées via `Assume`, 6 tests chacune) ; **lint vert
|
||||||
obligatoire** : `./gradlew lint` (v1.3.4 — `StringFormatMatches` aurait
|
obligatoire** : `./gradlew lint` (v1.3.4 — `StringFormatMatches` aurait
|
||||||
attrapé les crashs #47/#48 dès v1.3.1). Paliers de test du projet :
|
attrapé les crashs #47/#48 dès v1.3.1). Paliers de test du projet :
|
||||||
@ -1496,14 +1516,19 @@ Sur le téléphone de test (à compléter par l'utilisatrice) :
|
|||||||
Import → langue/seuils restaurés ; un VIEUX backup (v1.4.1) s'importe
|
Import → langue/seuils restaurés ; un VIEUX backup (v1.4.1) s'importe
|
||||||
sans les réglages (rétrocompat) ; smoke-test R8 : export/import
|
sans les réglages (rétrocompat) ; smoke-test R8 : export/import
|
||||||
(réflexion Gson sur UserSettings, `-keep` ajouté)
|
(réflexion Gson sur UserSettings, `-keep` ajouté)
|
||||||
|
- [ ] **v1.4.3** : **delta 6 h de l'accueil** : le lendemain d'une injection
|
||||||
|
EEn (plateau), la carte « niveau actuel » doit comparer à il y a 6 h
|
||||||
|
RÉELLEMENT (bug #53 : avant, la comparaison portait sur 24 h et le
|
||||||
|
delta pouvait être trompeusement négatif) — vérifier la cohérence avec
|
||||||
|
la courbe 24 h du graphique
|
||||||
|
|
||||||
---
|
---
|
||||||
*Doc mise à jour le 6 sept. 2026 (v1.4.2) — build OK, lint vert, 129/129 tests verts (111 sans
|
*Doc mise à jour le 7 sept. 2026 (v1.4.3) — build OK, lint vert, 137/137 tests verts (115 sans
|
||||||
les données locales), dépôts Gitea (cloudyfy + farewell) avec releases APK, aucune donnée
|
les données locales), dépôts Gitea (cloudyfy + farewell) avec releases APK, aucune donnée
|
||||||
de santé dans le dépôt ni l'historique. Fil des corrections : v1.3.3 = reprise de
|
de santé dans le dépôt ni l'historique. Fil des corrections : v1.3.3 = reprise de
|
||||||
maintenance (permissions agenda, export logs IO, bump de version) ; v1.3.4 = 2 crashs
|
maintenance (permissions agenda, export logs IO, bump de version) ; v1.3.4 = 2 crashs
|
||||||
reproduits sur émulateur (#47 stringResource arity, #48 LocalDate+pattern horaire), lint
|
reproduits sur émulateur (#47 stringResource arity, #48 LocalDate+pattern horaire), lint
|
||||||
filet bloquant ; v1.3.5 = diagnostic à distance via les logs exportés (#50/#50bis/#51
|
filet bloquant ; v1.3.5 = diagnostic à distance via les logs exportés (#50/#50bis/#51
|
||||||
agenda) ; v1.4.0 = modèle TFS V3C sur la méta-analyse officielle (7 esters, fidélité
|
agenda) ; v1.4.0 = modèle TFS V3C sur la méta-analyse officielle (7 esters, fidélité
|
||||||
~1 % épinglée) + rappels sur la grille Posologie (#52) ; v1.4.1 = prévision étendue sans saut (scroll futur, horizon 1 an) + delta en jours sur l'accueil ; v1.4.2 = seuils d'alerte configurables (carte accueil + notification WorkManager 15 min + anti-spam) et backup JSON v2 avec paramètres. Dette connue : fragments de
|
~1 % épinglée) + rappels sur la grille Posologie (#52) ; v1.4.1 = prévision étendue sans saut (scroll futur, horizon 1 an) + delta en jours sur l'accueil ; v1.4.2 = seuils d'alerte configurables (carte accueil + notification WorkManager 15 min + anti-spam) et backup JSON v2 avec paramètres ; v1.4.3 = fix du delta 6 h (#53) + régression n°4 (plateau EEn épinglé). Dette connue : fragments de
|
||||||
labs dans l'historique git (v1.1.0→v1.2.3) — cf §8.bis.*
|
labs dans l'historique git (v1.1.0→v1.2.3) — cf §8.bis.*
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user