diff --git a/README.md b/README.md index d169a95..ac49201 100644 --- a/README.md +++ b/README.md @@ -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 && 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) : diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2afaab6..e4fd41d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 { diff --git a/app/src/main/java/com/hormonetrack/ui/components/CurveChart.kt b/app/src/main/java/com/hormonetrack/ui/components/CurveChart.kt index e7a4dbf..b950177 100644 --- a/app/src/main/java/com/hormonetrack/ui/components/CurveChart.kt +++ b/app/src/main/java/com/hormonetrack/ui/components/CurveChart.kt @@ -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 diff --git a/app/src/main/java/com/hormonetrack/ui/screens/ChartScreen.kt b/app/src/main/java/com/hormonetrack/ui/screens/ChartScreen.kt index 0fa73bc..ae59af2 100644 --- a/app/src/main/java/com/hormonetrack/ui/screens/ChartScreen.kt +++ b/app/src/main/java/com/hormonetrack/ui/screens/ChartScreen.kt @@ -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>>(), @@ -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 } diff --git a/app/src/test/java/com/hormonetrack/ui/ChartScreenSourceGuardTest.kt b/app/src/test/java/com/hormonetrack/ui/ChartScreenSourceGuardTest.kt new file mode 100644 index 0000000..8540953 --- /dev/null +++ b/app/src/test/java/com/hormonetrack/ui/ChartScreenSourceGuardTest.kt @@ -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) + ) + } +} diff --git a/app/src/test/java/com/hormonetrack/ui/ChartZoomTest.kt b/app/src/test/java/com/hormonetrack/ui/ChartZoomTest.kt index 69289f3..ba8079f 100644 --- a/app/src/test/java/com/hormonetrack/ui/ChartZoomTest.kt +++ b/app/src/test/java/com/hormonetrack/ui/ChartZoomTest.kt @@ -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)) + } } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 0f39b09..23b485b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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) diff --git a/docs/DEVELOPPEMENT.md b/docs/DEVELOPPEMENT.md index f1c75f8..90543f5 100644 --- a/docs/DEVELOPPEMENT.md +++ b/docs/DEVELOPPEMENT.md @@ -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 : diff --git a/docs/GUIDE_INSTALLATION.md b/docs/GUIDE_INSTALLATION.md index 170722c..262dfc5 100644 --- a/docs/GUIDE_INSTALLATION.md +++ b/docs/GUIDE_INSTALLATION.md @@ -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