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:
Siphonight 2026-09-07 07:35:54 +02:00
parent 633c6debf4
commit a6ed2da96a
8 changed files with 292 additions and 15 deletions

View File

@ -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
> 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)
- **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)
@ -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
echo "sdk.dir=/chemin/vers/android-sdk" > local.properties # ou ANDROID_HOME
./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
```
@ -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) —
`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)) :
`-release.apk` (**recommandé**, optimisé R8, 2,4 Mo) et `-debug.apk` (20 Mo) :

View File

@ -17,8 +17,8 @@ android {
// tag : v1.3.0→v1.3.2 contenaient tous versionCode 14 / "1.3.0"
// (bump jamais commité) → BuildConfig.VERSION_NAME était faux dans
// les APK publiés (Paramètres + titre du dialog « Nouveautés »).
versionCode = 22
versionName = "1.4.2"
versionCode = 23
versionName = "1.4.3"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

View File

@ -100,6 +100,33 @@ fun forecastHorizonHours(maxIntervalDays: Double?): Long? {
fun clampPanHours(panHours: Long, futureHorizonHours: Long, maxPastHours: Long): Long =
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 :
* - axe gauche = E2 (pg/mL), axe droit = T (ng/mL) ;

View File

@ -346,9 +346,11 @@ private fun List<LabResult>.filterInRange(curve: List<LevelPoint>, marker: Strin
@Composable
private fun NowLevelCard(curve: List<LevelPoint>) {
val now = curve.lastOrNull()
val sixHAgo = curve.firstOrNull { p ->
(curve.last().timestamp - p.timestamp) >= 6 * HOUR_MS
}
// v1.4.3 (fix #53) : le point de comparaison « il y a 6 h » est celui le
// 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(
Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)

View 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)
}
}

View File

@ -76,4 +76,47 @@ class ChartZoomTest {
// bornes dégénérées (jamais négatives)
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)
}
}

View File

@ -1,7 +1,33 @@
# 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.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)

View File

@ -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.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. |
| 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. |
| 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. |
@ -409,7 +410,7 @@ Cp(t) = D·k1·k2 · [ e^(−k1·t)/((k1−k2)(k1−k3)) + e^(−k3·t)/((k1−k
## 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,
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 =
@ -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
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).
- **`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`) :
HIGH/LOW, comparaison STRICTE (valeur == limite → rien, éviter le
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
`settings`, `treatment_edit/{id}` (-1 = nouveau) ; barre masquée sur ces 2 routes
- `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
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 :
@ -1049,6 +1059,16 @@ l'exemple) :**
**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 :
« je m'injecte tous les samedis à 18 h → je dois recevoir le rappel
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`,
`versionName = "X.Y.Z+1"` (SemVer : fix = Z, feature = Y).
2. **Tests verts obligatoires** : `./gradlew testDebugUnitTest` — 129 au
total, 111 si `local-test-data/` est absent (les 3 classes de régression
2. **Tests verts obligatoires** : `./gradlew testDebugUnitTest` — 137 au
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
obligatoire** : `./gradlew lint` (v1.3.4 — `StringFormatMatches` aurait
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
sans les réglages (rétrocompat) ; smoke-test R8 : export/import
(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
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
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
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.*