v1.4.4 : fix #54 — prévision inaccessible depuis v1.4.1 : (a) captures FIGÉES dans pointerInput(Unit) (futurePanHorizon lu à 0 pour toujours → pan futur interdit) → rememberUpdatedState + garde de source ChartScreenSourceGuardTest ; (b) toggle sans effet visible (endMs=now → 0 dose générée) → forecastExtensionHours (extension jusqu'au 1er créneau, sans déplacer le début) ; 3 tests (140 verts) + lint ; validé émulateur (drag gauche → panHours<0, courbe dessinée au-delà de maintenant)

This commit is contained in:
Siphonight 2026-09-07 07:56:32 +02:00
parent 755d8f0007
commit 54b1cec532
9 changed files with 252 additions and 26 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.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** ✅
- **Statut** : v1.4.4 — build Android ✅, **lint vert** ✅, **140 tests unitaires** ✅ (118 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)
@ -39,8 +39,9 @@ sauvegarde JSON. **100 % local, aucun compte, aucun serveur.**
patch et voie orale
- **Simulation prévisionnelle** : configurer la **posologie** (intervalle en jours) sur un
traitement → projection des doses à venir sur le graphique (jamais sauvegardées) ;
**l'activation ne déplace pas le graphique** et la projection se parcourt en tirant
vers la gauche, jusqu'à **1 an** (v1.4.1)
**l'activation ne déplace pas le début du graphique** mais ÉTEND la fenêtre
jusqu'à la prochaine dose (v1.4.4) et la projection se parcourt en tirant
vers la gauche, jusqu'à **1 an**
- **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é
@ -88,7 +89,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 # 137 tests (115 sans les données locales)
./gradlew testDebugUnitTest # 140 tests (118 sans les données locales)
./gradlew lint # lint vert obligatoire avant release
```
@ -100,7 +101,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.3`) et **deux APK par release** (depuis v1.2.5)
**releases taguées** (`v1.1.0` → `v1.4.4`) 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 = 23
versionName = "1.4.3"
versionCode = 24
versionName = "1.4.4"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

View File

@ -90,6 +90,38 @@ fun forecastHorizonHours(maxIntervalDays: Double?): Long? {
return (maxIntervalDays * 24.0 * 12.0).toLong().coerceIn(24L * 30, 24L * 365)
}
/**
* EXTENSION de fenêtre à droite quand le chip Prévision est activé
* (v1.4.4, fix #54) : le toggle doit avoir un effet IMMÉDIAT et visible.
*
* ⚠️ Historique : en v1.4.1, activer la prévision ne déplaçait plus la
* fenêtre (le saut « tout à droite » de v1.4.0 avait été rejeté) MAIS la
* projection ne devenait visible qu'en tirant vers la gauche — car
* `endMs = now` faisait que `generateForecastDoses(..., toMs = now, ...)`
* ne générait AUCUNE dose : le chip semblait inerte (« le toggle ne fait
* plus rien », remontée v1.4.3). La solution : étendre la fenêtre à droite
* SANS déplacer son début (l'historique visible reste à sa place, seule la
* largeur augmente) jusqu'à couvrir au moins la prochaine dose projetée,
* sinon d'un tiers de la plage actuelle.
*
* @param rangeHours plage courante (24 h / 7 j / 30 j…)
* @param horizonHours horizon de prévision ([forecastHorizonHours], ≤ 0 = pas de prévision)
* @param firstSlotGapHours écart « maintenant → premier créneau projeté »
* (null = aucun traitement avec Posologie)
* @return heures à ajouter à `endMs` (0 = rien).
*/
fun forecastExtensionHours(
rangeHours: Long,
horizonHours: Long,
firstSlotGapHours: Long?
): Long {
if (horizonHours <= 0L) return 0L
// Montrer AU MOINS la prochaine dose (+12 h de contexte), sinon 1/3 de
// la plage actuelle — jamais au-delà de l'horizon.
val toFirstSlot = (firstSlotGapHours ?: 0L) + 12L
return maxOf(rangeHours / 3, toFirstSlot).coerceIn(0L, horizonHours)
}
/**
* Bornes du panoramique (v1.4.1) : la fenêtre peut reculer dans le PASSÉ
* jusqu'à [0 ; maxPastHours] et — quand la prévision est active — AVANCER

View File

@ -31,6 +31,7 @@ import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
@ -127,20 +128,44 @@ fun ChartScreen() {
val maxIntervalDays = treatments.mapNotNull { it.forecastIntervalDays }.maxOrNull()
val forecastHorizonHours = com.hormonetrack.ui.components.forecastHorizonHours(maxIntervalDays)
// ── Fenêtre temporelle (v1.4.1) ──────────────────────────────────────────
// endMs = now − panHours (panHours > 0 = passé, < 0 = futur via prévision).
// ⚠️ ACTIVER la prévision ne déplace PLUS la fenêtre (ancien bug UX : le
// graphique sautait « tout à droite » dans le futur) : la courbe projetée
// s'étend au-delà de la fenêtre courante et on la parcourt en tirant vers
// la gauche (panHours négatif), borné par l'horizon. La fenêtre reste où
// l'utilisateur l'avait laissée.
// ── Fenêtre temporelle (v1.4.1, extension v1.4.4 — fix #54) ──────────────
// endMs = now − panHours + extension (panHours > 0 = passé, < 0 = futur
// via prévision). Le drag vers la gauche parcourt la projection (borné
// par l'horizon) ; ⚠️ ACTIVER la prévision ne déplace JAMAIS le début de
// la fenêtre (pas de saut, v1.4.1) mais l'ÉTEND à droite d'après
// [forecastExtensionHours] : jusqu'au premier créneau projeté (+12 h de
// contexte), sinon 1/3 de la plage — sinon `generateForecastDoses`
// (borné à endMs) ne générait AUCUNE dose et le chip semblait inerte.
val nowMs = System.currentTimeMillis()
val earliestDoseMs = doseLogs.minOfOrNull { it.timestamp } ?: nowMs
val maxPanHours = (((nowMs - earliestDoseMs) / HOUR_MS) + rangeHours / HOUR_MS).coerceAtLeast(0L)
val futurePanHorizon = if (showForecast) (forecastHorizonHours ?: 0L) else 0L
val endMs = nowMs - panHours * HOUR_MS
val forecastExtensionH = if (showForecast && forecastHorizonHours != null) {
val firstSlotGapH = treatments.flatMap { tr ->
PharmacokineticEngine.generateForecastDoses(
tr, doseLogs, nowMs + forecastHorizonHours * HOUR_MS, nowMs
)
}.minOfOrNull { it.timestamp }?.let { (it - nowMs) / HOUR_MS }
com.hormonetrack.ui.components.forecastExtensionHours(
rangeHours, forecastHorizonHours, firstSlotGapH
)
} else 0L
val endMs = nowMs - panHours * HOUR_MS + forecastExtensionH * HOUR_MS
val startMs = endMs - rangeHours * HOUR_MS
// ⚠️ FIX #54 (v1.4.4) — captures FIGÉES dans pointerInput(Unit) : la
// closure du gestionnaire de gestes est créée UNE SEULE FOIS ; les vals
// calculées ci-dessus y étaient capturées à leur valeur de la PREMIÈRE
// composition (showForecast = false au lancement → futurePanHorizon = 0)
// → le clamp `coerceIn(0, maxPan)` INTERDISAIT le pan vers le futur pour
// toujours : le chip activé + le drag vers la gauche ne généraient JAMAIS
// la projection (« le toggle ne fait rien »). rememberUpdatedState expose
// ces valeurs comme des états lus À JOUR par la closure (les MutableState
// — panHours, rangeHours, showForecast — n'ont pas ce problème).
val gestureFutureHorizon by rememberUpdatedState(futurePanHorizon)
val gestureMaxPan by rememberUpdatedState(maxPanHours)
val gestureExtension by rememberUpdatedState(forecastExtensionH)
// Une liste de courbes par modèle sélectionné (clé = "ESE" / "TFS")
val curves by produceState(
emptyList<Pair<String, List<LevelPoint>>>(),
@ -324,7 +349,7 @@ fun ChartScreen() {
val hoursDragged = (pan.x / chartWidthPx * rangeHours).toLong()
if (hoursDragged != 0L) {
panHours = com.hormonetrack.ui.components.clampPanHours(
panHours + hoursDragged, futurePanHorizon, maxPanHours
panHours + hoursDragged, gestureFutureHorizon, gestureMaxPan
)
}
// Zoom autour du point focal : le temps sous le doigt
@ -334,12 +359,15 @@ fun ChartScreen() {
val newRange = (rangeHours / zoom).toLong()
.coerceIn(MIN_RANGE_H, MAX_RANGE_H)
if (newRange != rangeHours) {
val endAnchor = nowMs - panHours * HOUR_MS
// L'ancre de droite inclut l'extension de
// prévision (v1.4.4) : cohérent avec endMs
val endAnchor = nowMs - panHours * HOUR_MS +
gestureExtension * HOUR_MS
val tFocal = endAnchor - ((1.0 - f) * rangeHours * HOUR_MS).toLong()
val newEnd = tFocal + ((1.0 - f) * newRange * HOUR_MS).toLong()
val newPan = ((nowMs - newEnd) / HOUR_MS).toLong()
panHours = com.hormonetrack.ui.components.clampPanHours(
newPan, futurePanHorizon, maxPanHours
newPan, gestureFutureHorizon, gestureMaxPan
)
rangeHours = newRange
}

View File

@ -0,0 +1,79 @@
package com.hormonetrack.ui
import org.junit.Assert.assertTrue
import org.junit.Test
import java.io.File
/**
* **Garde de source** (v1.4.4, bug #54) : les valeurs de FENÊTRE du graphique
* (`futurePanHorizon`, `maxPanHours`, `forecastExtensionH`) sont des `val`
* calculées à chaque recomposition. Le gestionnaire de gestes vit dans
* `pointerInput(Unit)` — sa closure est créée UNE SEULE FOIS : si ces vals y
* sont lues directement, la closure garde leur valeur de la PREMIÈRE
* composition (stale) → `futurePanHorizon = 0` au lancement → le pan vers le
* futur (prévision) devenait DÉFINITIVEMENT impossible, même après activation
* du chip (remontée v1.4.3 : « le toggle ne génère pas les prévisions »).
*
* La correction (cf ChartScreen.kt) expose ces valeurs via
* `rememberUpdatedState` sous des alias `gesture*` que la closure lit à jour.
* Le compilateur/lint Compose ne détecte PAS ce pattern de capture figée :
* ce test est le filet — il fait échouer toute réintroduction avec un
* message explicite.
*/
class ChartScreenSourceGuardTest {
private fun chartSource(): String {
val candidates = listOf(
File("src/main/java/com/hormonetrack/ui/screens/ChartScreen.kt"),
File("app/src/main/java/com/hormonetrack/ui/screens/ChartScreen.kt")
)
val f = candidates.firstOrNull { it.exists() }
?: error("ChartScreen.kt introuvable (contextes testés : $candidates)")
return f.readText()
}
/** Extrait le corps du gestionnaire de gestes (closure `detectTransformGestures`). */
private fun gestureBlock(src: String): String {
// ⚠️ cibler l'APPEL (suivi d'une accolade) et non l'import du même nom
val start = src.indexOf("detectTransformGestures {")
if (start < 0) error("detectTransformGestures introuvable dans ChartScreen.kt — l'API a-t-elle changé ? Mets ce garde à jour.")
// compter les accolades depuis la première ouverte après le marqueur
val open = src.indexOf('{', start)
var depth = 0
for (i in open until src.length) {
when (src[i]) {
'{' -> depth++
'}' -> { depth--; if (depth == 0) return src.substring(start, i + 1) }
}
}
error("bloc detectTransformGestures non fermé ?")
return ""
}
@Test
fun `window vals are exposed via rememberUpdatedState (not read stale in the gesture closure)`() {
val src = chartSource()
val block = gestureBlock(src)
// 1. les alias gesture* existent et sont déclarés via rememberUpdatedState
for (alias in listOf("gestureFutureHorizon", "gestureMaxPan", "gestureExtension")) {
assertTrue(
"alias $alias manquant — cf fix #54 : pointerInput(Unit) fige les captures",
Regex("val\\s+$alias\\s+by\\s+rememberUpdatedState").containsMatchIn(src)
)
}
// 2. la closure du geste ne lit JAMAIS les vals brutes (captures stale)
for (stale in listOf("futurePanHorizon", "maxPanHours", "forecastExtensionH")) {
val leak = Regex("""\b$stale\b""").findAll(block).any()
if (leak) {
error("BUG #54 réintroduit : la closure detectTransformGestures lit " +
"`$stale` directement (valeur figée à la 1re composition). " +
"Passe par l'alias rememberUpdatedState (gesture*) — cf ChartScreen.kt.")
}
}
// 3. la closure lit bien les alias à jour
assertTrue(
"la closure du geste doit utiliser les alias gesture* (lectures à jour)",
Regex("""\bgesture(FutureHorizon|MaxPan|Extension)\b""").containsMatchIn(block)
)
}
}

View File

@ -119,4 +119,35 @@ class ChartZoomTest {
val chosen2 = com.hormonetrack.ui.components.pointHoursBefore(last, 6.0)!!
assertEquals(3L * 3_600_000L, chosen2.timestamp)
}
// ── v1.4.4 (fix #54) : extension de fenêtre à l'activation de la prévision ──
@Test
fun `forecast extension - covers at least the next projected slot plus context`() {
val H = 24L * 3600_000L / 3600_000L // pas utilisé ; les valeurs sont en heures
// Posologie 6 j : prochain créneau à ~130 h → extension = 130+12 = 142 h
// (la prochaine dose devient VISIBLE dès l'activation du chip)
assertEquals(142L, com.hormonetrack.ui.components.forecastExtensionHours(
rangeHours = 24L, horizonHours = 24L * 365, firstSlotGapHours = 130L))
// fenêtre large (7 j) : 1/3 = 56 h vs prochain créneau à 20 h (+12) = 32 h
// → on garde le plus grand (56 h) pour garder du contexte
assertEquals(56L, com.hormonetrack.ui.components.forecastExtensionHours(
rangeHours = 24L * 7, horizonHours = 24L * 365, firstSlotGapHours = 20L))
}
@Test
fun `forecast extension - bounded by horizon, zero without forecast`() {
// jamais au-delà de l'horizon
assertEquals(24L * 72, com.hormonetrack.ui.components.forecastExtensionHours(
rangeHours = 24L * 7, horizonHours = 24L * 72, firstSlotGapHours = 24L * 100))
// horizon ≤ 0 ou premier créneau inconnu : garde-fous
assertEquals(0L, com.hormonetrack.ui.components.forecastExtensionHours(
rangeHours = 24L, horizonHours = 0L, firstSlotGapHours = 130L))
// sans créneau connu : garde 12 h de contexte (max(8, 12) = 12)
assertEquals(12L, com.hormonetrack.ui.components.forecastExtensionHours(
rangeHours = 24L, horizonHours = 24L * 365, firstSlotGapHours = null))
// créneau immédiat (gap 0) : max(2, 12) = 12 h de contexte
assertEquals(12L, com.hormonetrack.ui.components.forecastExtensionHours(
rangeHours = 6L, horizonHours = 24L, firstSlotGapHours = 0L))
}
}

View File

@ -1,7 +1,38 @@
# 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.3`, …).
Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.4.4`, …).
## [1.4.4] — 2026-09-07 (versionCode 24)
### Corrigé
- **La prévision était inaccessible depuis la v1.4.1** (#54, remontée : « en
1.4.3 je ne peux même pas drag le graphique vers la gauche pour voir les
prévisions ; le toggle activé ne génère pas les prévisions ») : **deux
causes emboîtées** —
1. **Captures FIGÉES dans le gestionnaire de gestes** : la closure de
`pointerInput(Unit)` est créée une seule fois et les `val` calculées
(`futurePanHorizon`…) y étaient figées à leur valeur de la PREMIÈRE
composition (0, chip désactivé) → le clamp interdisait DÉFINITIVEMENT
le pan vers le futur. FIX : les vals de fenêtre sont exposées au geste
via `rememberUpdatedState` (alias `gesture*`, lectures à jour).
**Garde de non-régression** : `ChartScreenSourceGuardTest` fait échouer
toute réintroduction de lecture figée dans la closure (le lint Compose
ne détecte pas ce pattern).
2. **Toggle sans effet visible** : à l'activation, `endMs = now` faisait
que `generateForecastDoses(..., toMs = now, ...)` ne générait AUCUNE
dose. FIX : à l'activation, la fenêtre s'ÉTEND à droite (sans déplacer
son début — pas de saut) jusqu'au premier créneau projeté (+12 h de
contexte), sinon 1/3 de la plage, borné par l'horizon
(`forecastExtensionHours`, testé). Désactiver → retour à la fenêtre
standard. Le drag vers la gauche continue au-delà (jusqu'à 1 an).
### Ajouté
- **3 nouveaux tests** (140 au total / 118 sans les données locales) :
extension de fenêtre (bornes, garde-fous) et garde de source anti-captures
figées. Validation émulateur : drag gauche → panHours < 0 (« Revenir à
maintenant » apparaît) et la courbe est dessinée au-delà de la ligne
« maintenant » (analyse du screencap).
## [1.4.3] — 2026-09-07 (versionCode 23)

View File

@ -86,6 +86,7 @@ Fonctionnalités v1 :
| 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. |
| 7 sept. 2026 (session v1.4.4) | **Prévision inaccessible depuis v1.4.1** (#54, remontée) : DEUX causes emboîtées — (a) captures FIGÉES dans `pointerInput(Unit)` (`futurePanHorizon` lu à 0 pour toujours → pan futur interdit) → `rememberUpdatedState` + **garde de source** (`ChartScreenSourceGuardTest`, le lint Compose ne voit pas ce pattern) ; (b) toggle sans effet visible (`endMs = now` → 0 dose générée) → extension contrôlée à droite (`forecastExtensionHours`, jusqu'au 1ᵉʳ créneau, sans déplacer le début). Validé émulateur : drag gauche → panHours < 0, courbe dessinée au-delà de « maintenant » (screencap). 140 verts. Publication v1.4.4. |
| 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. |
@ -410,7 +411,7 @@ Cp(t) = D·k1·k2 · [ e^(−k1·t)/((k1−k2)(k1−k3)) + e^(−k3·t)/((k1−k
## 8. Tests unitaires
**137 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — **115 sans
**140 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — **118 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 =
@ -533,6 +534,11 @@ 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).
- **`ChartScreenSourceGuardTest`** (1) : v1.4.4 — **garde de SOURCE** contre
le bug #54 : vérifie que les vals de fenêtre du graphique sont exposées au
gestionnaire de gestes via `rememberUpdatedState` (alias `gesture*`) et que
la closure `detectTransformGestures` ne les lit jamais en direct (captures
figées). Le lint Compose ne détecte pas ce pattern — ce test est le filet.
- **`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
@ -1071,6 +1077,23 @@ l'exemple) :**
**Session v1.4.0 (modèle TFS sur la méta-analyse + fix #52) :**
54. **Prévision inaccessible : captures figées dans `pointerInput(Unit)`**
(v1.4.4, remontée « le toggle activé ne génère pas les prévisions, je ne
peux même pas dragger vers la gauche ») : la closure du gestionnaire de
gestes est créée UNE FOIS (`pointerInput(Unit)`) ; les `val` calculées
de la composition (`futurePanHorizon`, `maxPanHours`, `forecastExtensionH`)
y étaient FIGÉES à leur valeur initiale (0, chip désactivé) → le clamp
interdisait le pan vers le futur POUR TOUJOURS, même après activation du
chip. Fix : `rememberUpdatedState` (les `MutableState` n'ont pas le
problème : c'est la fermeture `by` qui lit à jour). + cause secondaire :
`generateForecastDoses(toMs = endMs = now)` ne générait rien → extension
contrôlée à l'activation (cf §10). **Garde de source** :
`ChartScreenSourceGuardTest` (extraire la closure et interdire les
lectures des vals brutes). Leçon : dans `pointerInput(Unit)`, TOUTE val
calculée lue par la closure doit passer par `rememberUpdatedState` —
seul `pointerInput(key)` re-keyé relance la closure (et perd les gestes
en cours).
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
@ -1240,8 +1263,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` — 137 au
total, 115 si `local-test-data/` est absent (les 3 classes de régression
2. **Tests verts obligatoires** : `./gradlew testDebugUnitTest` — 140 au
total, 118 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 :

View File

@ -178,10 +178,11 @@ Pics de référence (dose unique de 5 mg, IM) :
- **Prévision** : configure la **Posologie** (intervalle en jours) dans un traitement
(section « Fréquence ») puis active le chip « Prévision » → les doses à venir sont
simulées et dessinées après la ligne « maintenant » (jamais sauvegardées).
**Activer le chip ne déplace pas le graphique** : tire vers la GAUCHE pour parcourir
la projection dans le futur (jusqu'à 1 an selon la Posologie) ; « Revenir à
maintenant » pour revenir. Si tu désactives le chip alors que tu es dans le futur,
l'app revient automatiquement à maintenant
**Activer le chip étend le graphique jusqu'à ta prochaine dose** (sans déplacer
l'historique visible) — v1.4.4 : le chip a un effet immédiat ; tire vers la GAUCHE
pour parcourir la projection dans le futur (jusqu'à 1 an selon la Posologie) ;
« Revenir à maintenant » pour revenir. Si tu désactives le chip alors que tu es
dans le futur, l'app revient automatiquement à maintenant
- **Pics / creux** (chip sur le graphique) : triangles ▲▼ aux extrema estimés de
chaque courbe (E2 et T, les deux modèles) — pratique pour visualiser d'un coup
d'œil les hauts et les bas entre deux injections