From 922d49841f63d913b7cbb07e378a26fc95a18496 Mon Sep 17 00:00:00 2001 From: Siphonight Date: Sun, 6 Sep 2026 23:00:42 +0200 Subject: [PATCH] =?UTF-8?q?v1.4.1=20:=20pr=C3=A9vision=20=C3=A9tendue=20et?= =?UTF-8?q?=20sans=20saut=20=E2=80=94=20activer=20le=20chip=20ne=20d=C3=A9?= =?UTF-8?q?place=20plus=20la=20fen=C3=AAtre=20(endMs=20=3D=20now=20?= =?UTF-8?q?=E2=88=92=20panHours=20sign=C3=A9,=20panHours=20<=200=20=3D=20f?= =?UTF-8?q?utur=20born=C3=A9=20par=20l'horizon=2012=C3=97Posologie=20[30?= =?UTF-8?q?=20j=20;=201=20an],=20clamp=20unique=20clampPanHours)=20;=20del?= =?UTF-8?q?ta=20en=20jours=20sur=20la=20carte=20Prochaine=20dose=20(>=2024?= =?UTF-8?q?=20h=20:=20'5=20j=202=20h=20=C2=B7=20sam.=206=2018:00')=20;=205?= =?UTF-8?q?=20tests=20(112=20verts)=20+=20lint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 13 +++-- app/build.gradle.kts | 4 +- .../hormonetrack/ui/components/CurveChart.kt | 23 +++++++++ .../hormonetrack/ui/screens/ChartScreen.kt | 50 +++++++++++++------ .../com/hormonetrack/ui/screens/HomeScreen.kt | 36 +++++++++---- .../java/com/hormonetrack/util/HrtDuration.kt | 8 +++ app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + .../java/com/hormonetrack/ui/ChartZoomTest.kt | 48 ++++++++++++++++++ .../com/hormonetrack/util/HrtDurationTest.kt | 12 +++++ docs/CHANGELOG.md | 25 +++++++++- docs/DEVELOPPEMENT.md | 35 ++++++++++--- docs/GUIDE_INSTALLATION.md | 8 ++- 13 files changed, 221 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 52478e3..e629c85 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.0 — build Android ✅, **lint vert** ✅, **107 tests unitaires** ✅ (89 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.1 — build Android ✅, **lint vert** ✅, **112 tests unitaires** ✅ (94 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) @@ -38,7 +38,9 @@ sauvegarde JSON. **100 % local, aucun compte, aucun serveur.** - **Modèle Bateman** paramétrable (temps au pic, demi-vie, biodisponibilité) pour gel, 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) + 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) - **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é @@ -62,7 +64,8 @@ sauvegarde JSON. **100 % local, aucun compte, aucun serveur.** 7 jours ne sonne que le jour d'injection, pas tous les jours - **Nouveautés à chaque mise à jour** : dialog de changelog automatique (fermé = ne réapparaît pas avant la prochaine version) -- **Temps sous THS** affiché en haut de la page Doses (depuis la 1re prise) +- **Temps sous THS** affiché en haut de la page Doses (depuis la 1re prise) ; + **prochaine dose en jours** sur l'accueil quand elle est à plus de 24 h (v1.4.1) - **Logs de diagnostic** exportables (Paramètres) — utile pour le support - **Événements d'agenda** : les rappels de prises peuvent créer un événement récurrent (posologie) dans un calendrier « HormoneTrack » de ton téléphone @@ -79,7 +82,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 # 107 tests (89 sans les données locales) +./gradlew testDebugUnitTest # 112 tests (94 sans les données locales) ./gradlew lint # lint vert obligatoire avant release ``` @@ -91,7 +94,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.0`) et **deux APK par release** (depuis v1.2.5) +**releases taguées** (`v1.1.0` → `v1.4.1`) 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 6e13f1a..b15a93c 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 = 20 - versionName = "1.4.0" + versionCode = 21 + versionName = "1.4.1" 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 9c546a9..40509bc 100644 --- a/app/src/main/java/com/hormonetrack/ui/components/CurveChart.kt +++ b/app/src/main/java/com/hormonetrack/ui/components/CurveChart.kt @@ -77,6 +77,29 @@ fun stepForRange(rangeHours: Long): Long = when { else -> 3_600_000L // 1 h } +/** + * Horizon de PRÉVISION en heures (v1.4.1) : quantité de doses projetées + * générées au-delà de « maintenant » quand le chip Prévision est actif. + * 12 × la plus grande Posologie, borné [30 j ; 365 j] — « scroller à + * l'infini » s'arrête à 1 an (compromis calcul : 365 j / 6 j ≈ 61 doses + * projetées superposées, coût O(points × doses) maîtrisé). + * @return null si aucun traitement n'a de Posologie (chip désactivé). + */ +fun forecastHorizonHours(maxIntervalDays: Double?): Long? { + if (maxIntervalDays == null || maxIntervalDays <= 0.0) return null + return (maxIntervalDays * 24.0 * 12.0).toLong().coerceIn(24L * 30, 24L * 365) +} + +/** + * 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 + * dans le FUTUR jusqu'à [-futureHorizonHours ; …] (panHours négatif = + * fenêtre décalée vers le futur). Unique point de clamp partagé par le + * drag et le zoom pour éviter les divergences. + */ +fun clampPanHours(panHours: Long, futureHorizonHours: Long, maxPastHours: Long): Long = + panHours.coerceIn(-futureHorizonHours.coerceAtLeast(0L), maxPastHours.coerceAtLeast(0L)) + /** * Graphique Canvas pur, multi-séries : * - axe gauche = E2 (pg/mL), axe droit = T (ng/mL) ; 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 67b6a10..0fa73bc 100644 --- a/app/src/main/java/com/hormonetrack/ui/screens/ChartScreen.kt +++ b/app/src/main/java/com/hormonetrack/ui/screens/ChartScreen.kt @@ -122,16 +122,23 @@ fun ChartScreen() { } } - // Horizon de prévision : 2 × le plus grand intervalle configuré (borné 7–30 jours) + // Horizon de prévision (v1.4.1) : 12 × la plus grande Posologie, borné + // [30 j ; 365 j] (cf forecastHorizonHours dans CurveChart.kt). val maxIntervalDays = treatments.mapNotNull { it.forecastIntervalDays }.maxOrNull() - val forecastHorizonHours = maxIntervalDays?.let { (it * 24 * 2).toLong().coerceIn(DAY_MS / HOUR_MS * 7, DAY_MS / HOUR_MS * 30) } + val forecastHorizonHours = com.hormonetrack.ui.components.forecastHorizonHours(maxIntervalDays) - // Fenêtre temporelle courante : endMs recule quand on tire vers la droite + // ── 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. val nowMs = System.currentTimeMillis() val earliestDoseMs = doseLogs.minOfOrNull { it.timestamp } ?: nowMs val maxPanHours = (((nowMs - earliestDoseMs) / HOUR_MS) + rangeHours / HOUR_MS).coerceAtLeast(0L) - val endMs = nowMs - panHours * HOUR_MS + - (if (showForecast) (forecastHorizonHours ?: 0L) * HOUR_MS else 0L) + val futurePanHorizon = if (showForecast) (forecastHorizonHours ?: 0L) else 0L + val endMs = nowMs - panHours * HOUR_MS val startMs = endMs - rangeHours * HOUR_MS // Une liste de courbes par modèle sélectionné (clé = "ESE" / "TFS") @@ -266,7 +273,13 @@ fun ChartScreen() { ) FilterChip( selected = showForecast, - onClick = { showForecast = !showForecast }, + onClick = { + showForecast = !showForecast + // v1.4.1 : désactiver la prévision pendant qu'on est dans le + // futur → revenir à maintenant (sinon fenêtre vide au-delà + // de « maintenant » sans doses projetées). + if (!showForecast) panHours = maxOf(panHours, 0L) + }, enabled = forecastHorizonHours != null, label = { Text(stringResource(R.string.show_forecast)) } ) @@ -303,13 +316,16 @@ fun ChartScreen() { // Pan (1 doigt) + pinch-zoom (2 doigts, v1.2.9) — un seul // handler pour éviter les conflits de consommation detectTransformGestures { centroid, pan, zoom, _ -> - val forecastMs = - (if (showForecast) (forecastHorizonHours ?: 0L) * HOUR_MS else 0L) if (chartWidthPx > 0) { - // Pan horizontal : glisser à droite = remonter le temps + // Pan horizontal : glisser à DROITE = remonter le + // temps (panHours croît) ; glisser à GAUCHE = + // avancer dans le FUTUR (panHours décroît, borné + // par l'horizon de prévision — v1.4.1) val hoursDragged = (pan.x / chartWidthPx * rangeHours).toLong() if (hoursDragged != 0L) { - panHours = (panHours + hoursDragged).coerceIn(0L, maxPanHours) + panHours = com.hormonetrack.ui.components.clampPanHours( + panHours + hoursDragged, futurePanHorizon, maxPanHours + ) } // Zoom autour du point focal : le temps sous le doigt // reste fixe pendant le pinch @@ -318,11 +334,13 @@ fun ChartScreen() { val newRange = (rangeHours / zoom).toLong() .coerceIn(MIN_RANGE_H, MAX_RANGE_H) if (newRange != rangeHours) { - val endAnchor = nowMs + forecastMs - panHours * HOUR_MS + val endAnchor = nowMs - panHours * 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 + forecastMs - newEnd) / HOUR_MS).toLong() - panHours = newPan.coerceIn(0L, maxPanHours) + val newPan = ((nowMs - newEnd) / HOUR_MS).toLong() + panHours = com.hormonetrack.ui.components.clampPanHours( + newPan, futurePanHorizon, maxPanHours + ) rangeHours = newRange } } @@ -354,8 +372,10 @@ fun ChartScreen() { ) Spacer(Modifier.height(8.dp)) - // Retour au présent dès qu'on a voyagé dans le passé - if (panHours > 0L) { + // Retour au présent dès qu'on a voyagé (passé OU futur — + // v1.4.1 : le futur est atteint en tirant vers la gauche + // avec la prévision active) + if (panHours != 0L) { TextButton(onClick = { panHours = 0L }) { Text(stringResource(R.string.back_to_present)) } diff --git a/app/src/main/java/com/hormonetrack/ui/screens/HomeScreen.kt b/app/src/main/java/com/hormonetrack/ui/screens/HomeScreen.kt index cf531a8..9ac4b0c 100644 --- a/app/src/main/java/com/hormonetrack/ui/screens/HomeScreen.kt +++ b/app/src/main/java/com/hormonetrack/ui/screens/HomeScreen.kt @@ -362,17 +362,31 @@ private fun NextDoseCard(treatments: List, doseLogs: List) { val deltaMs = next - System.currentTimeMillis() val h = deltaMs / HOUR_MS val m = (deltaMs % HOUR_MS) / 60_000L - val timeStr = DateTimeFormatter.ofPattern("HH:mm") - .withZone(ZoneId.systemDefault()) - .format(Instant.ofEpochMilli(next)) - Text( - if (h > 0) { - String.format(Locale.getDefault(), "%dh%02d · %s (%s)", h, m, timeStr, nextTreatment?.name ?: "") - } else { - String.format(Locale.getDefault(), "%d min · %s (%s)", m, timeStr, nextTreatment?.name ?: "") - }, - style = MaterialTheme.typography.titleMedium - ) + val name = nextTreatment?.name ?: "" + // v1.4.1 : au-delà de 24 h, afficher en JOURS (les heures + // deviennent peu lisibles) + le jour de la semaine dans la date. + if (h >= 24) { + val (days, remH) = com.hormonetrack.util.HrtDuration.daysAndHours(h) + val dayTimeStr = DateTimeFormatter.ofPattern("EEE d HH:mm") + .withZone(ZoneId.systemDefault()) + .format(Instant.ofEpochMilli(next)) + Text( + stringResource(R.string.next_dose_days, days, remH, dayTimeStr, name), + style = MaterialTheme.typography.titleMedium + ) + } else { + val timeStr = DateTimeFormatter.ofPattern("HH:mm") + .withZone(ZoneId.systemDefault()) + .format(Instant.ofEpochMilli(next)) + Text( + if (h > 0) { + String.format(Locale.getDefault(), "%dh%02d · %s (%s)", h, m, timeStr, name) + } else { + String.format(Locale.getDefault(), "%d min · %s (%s)", m, timeStr, name) + }, + style = MaterialTheme.typography.titleMedium + ) + } } } } diff --git a/app/src/main/java/com/hormonetrack/util/HrtDuration.kt b/app/src/main/java/com/hormonetrack/util/HrtDuration.kt index 318ac14..cecd75e 100644 --- a/app/src/main/java/com/hormonetrack/util/HrtDuration.kt +++ b/app/src/main/java/com/hormonetrack/util/HrtDuration.kt @@ -12,4 +12,12 @@ object HrtDuration { /** Décomposition mois(30 j) / jours restants. */ fun monthsAndDays(totalDays: Long): Pair = Pair(totalDays / 30, totalDays % 30) + + /** + * Conversion d'un delta EN HEURES en (jours, heures restantes) — v1.4.1, + * carte « Prochaine dose » de l'accueil : au-delà de 24 h, les heures + * parlent moins que les jours (ex. 120 h → 5 j 0 h). + */ + fun daysAndHours(totalHours: Long): Pair = + Pair(totalHours / 24, totalHours % 24) } diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index f399dc4..c67aba1 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -17,6 +17,7 @@ Pas encore de données — crée un traitement et logue une dose. Log rapide Prochaine dose + %1$d j %2$d h · %3$s (%4$s) %1$s %2$s pg/mL vs il y a 6 h Dernières 24 h (estimation) — E2 estimé (pg/mL, gauche) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index df90c61..7d97f25 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -17,6 +17,7 @@ No data yet — add a treatment and log a dose. Quick log Next dose + %1$d d %2$d h · %3$s (%4$s) %1$s %2$s pg/mL vs 6 h ago Last 24 hours (estimate) — E2 estimate (pg/mL, left) diff --git a/app/src/test/java/com/hormonetrack/ui/ChartZoomTest.kt b/app/src/test/java/com/hormonetrack/ui/ChartZoomTest.kt index baedb79..6c878da 100644 --- a/app/src/test/java/com/hormonetrack/ui/ChartZoomTest.kt +++ b/app/src/test/java/com/hormonetrack/ui/ChartZoomTest.kt @@ -1,12 +1,18 @@ package com.hormonetrack.ui +import com.hormonetrack.ui.components.clampPanHours +import com.hormonetrack.ui.components.forecastHorizonHours import com.hormonetrack.ui.components.stepForRange import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull import org.junit.Test /** * Pas d'échantillonnage adaptatif au zoom (v1.2.9) : plus la fenêtre est * courte, plus le pas est fin — sinon les courbes paraissent anguleuses. + * + * + v1.4.1 : horizon de prévision (12 × Posologie, borné 30–365 j) et bornes + * du panoramique (panHours négatif = futur ; clamp unique drag/zoom). */ class ChartZoomTest { @@ -28,4 +34,46 @@ class ChartZoomTest { assertEquals(3_600_000L, stepForRange(24L * 30)) assertEquals(3_600_000L, stepForRange(24L * 300)) } + + @Test + fun `forecast horizon is null without posology`() { + assertNull(forecastHorizonHours(null)) + assertNull(forecastHorizonHours(0.0)) + assertNull(forecastHorizonHours(-2.0)) + } + + @Test + fun `forecast horizon is 12x the largest interval bounded 30d-365d`() { + // Posologie 6 j → 72 j d'horizon (12 × 6) + assertEquals(24L * 72, forecastHorizonHours(6.0)) + // petits intervalles → plancher 30 j (sinon horizon trop court) + assertEquals(24L * 30, forecastHorizonHours(1.0)) + // grands intervalles : 30 j × 12 = 360 j (sous le plafond)… + assertEquals(24L * 360, forecastHorizonHours(30.0)) + // …et plafond 1 an au-delà de ~30,4 j (l'« infini » s'arrête là) + assertEquals(24L * 365, forecastHorizonHours(31.0)) + assertEquals(24L * 365, forecastHorizonHours(90.0)) + } + + @Test + fun `pan clamps to the past only when forecast is off`() { + // Prévision OFF : futur interdit (horizon 0) → un drag négatif ramène à 0 + assertEquals(0L, clampPanHours(panHours = -48L, futureHorizonHours = 0L, maxPastHours = 100L)) + assertEquals(50L, clampPanHours(50L, 0L, 100L)) + // bornes passées respectées + assertEquals(100L, clampPanHours(500L, 0L, 100L)) + } + + @Test + fun `pan can go into the future up to the forecast horizon`() { + // Prévision ON (horizon 72 j) : panHours négatif = fenêtre décalée + // vers le futur — c'est ainsi qu'on « scrolle à droite » sans saut + // à l'activation du chip. + assertEquals(-24L * 72, clampPanHours(-24L * 500, 24L * 72, 100L)) + assertEquals(-5L, clampPanHours(-5L, 24L * 72, 100L)) + // le passé reste borné par maxPast + assertEquals(100L, clampPanHours(1000L, 24L * 72, 100L)) + // bornes dégénérées (jamais négatives) + assertEquals(0L, clampPanHours(-10L, -5L, -3L)) + } } diff --git a/app/src/test/java/com/hormonetrack/util/HrtDurationTest.kt b/app/src/test/java/com/hormonetrack/util/HrtDurationTest.kt index fab6d6d..9230bb4 100644 --- a/app/src/test/java/com/hormonetrack/util/HrtDurationTest.kt +++ b/app/src/test/java/com/hormonetrack/util/HrtDurationTest.kt @@ -35,4 +35,16 @@ class HrtDurationTest { fun `décomposition - zéro jour`() { assertEquals(Pair(0L, 0L), HrtDuration.monthsAndDays(0L)) } + + @Test + fun `heures en jours et heures restantes - carte prochaine dose v1_4_1`() { + // 120 h = 5 j 0 h + assertEquals(Pair(5L, 0L), HrtDuration.daysAndHours(120L)) + // 51 h = 2 j 3 h + assertEquals(Pair(2L, 3L), HrtDuration.daysAndHours(51L)) + // < 24 h → 0 j (l'écran utilise alors le format heures/minutes) + assertEquals(Pair(0L, 23L), HrtDuration.daysAndHours(23L)) + // zéro + assertEquals(Pair(0L, 0L), HrtDuration.daysAndHours(0L)) + } } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index e53fe7b..64fdf63 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,7 +1,30 @@ # 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.0`, …). +Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.4.1`, …). + +## [1.4.1] — 2026-09-06 (versionCode 21) + +### Ajouté +- **Prévision étendue ET sans saut temporel** (remontées utilisateur) : + - activer le chip **Prévision** ne déplace PLUS le graphique « tout à + droite » dans le futur — la fenêtre reste exactement où elle était ; + la courbe projetée s'étend au-delà et on la parcourt en **tirant vers + la gauche** (« scroller à l'infini » = jusqu'à 1 an de projection) ; + « Revenir à maintenant » ramène depuis le passé **ou** le futur ; + - l'horizon passe de « 2 × Posologie, borné 7–30 j » à **12 × Posologie, + borné 30 j – 1 an** (`forecastHorizonHours`, épinglé par `ChartZoomTest`). + - désactiver la prévision pendant qu'on est dans le futur → retour + automatique à « maintenant » (sinon fenêtre sans données). +- **Delta en JOURS sur la carte « Prochaine dose »** (remontée : les heures + deviennent illisibles au-delà de 24 h) : à partir d'un jour, la carte + affiche « 5 j 2 h · sam. 6 18:00 (traitement) » avec le jour de la + semaine ; en dessous, format heures/minutes inchangé + (`HrtDuration.daysAndHours` + string `next_dose_days` FR/EN). +- **5 nouveaux tests** (112 au total / 94 sans les données locales) : + bornes d'horizon et clamp de panoramique (`ChartZoomTest` ×4 — le clamp + unique drag/zoom est centralisé dans `clampPanHours`), + conversion heures→jours (`HrtDurationTest` ×1). ## [1.4.0] — 2026-09-06 (versionCode 20) diff --git a/docs/DEVELOPPEMENT.md b/docs/DEVELOPPEMENT.md index 6500ee3..574b703 100644 --- a/docs/DEVELOPPEMENT.md +++ b/docs/DEVELOPPEMENT.md @@ -83,6 +83,7 @@ Fonctionnalités v1 : | 6 sept. 2026 (session v1.3.4) | **Deux crashs v1.3.3 remontés (Doses + export logs)** → reproduits sur ÉMULATEUR avec les vraies données (recette §16.ter, désormais standard) : (a) Doses = string `hrt_duration` 3 placeholders vs 2 args (**né en v1.3.1**, §14 #47) ; (b) export logs = `LocalDate.format("…-HHmm")` levant `UnsupportedTemporalTypeException` au TAP (§14 #48) — les fixes IO v1.3.2/v1.3.3 étaient à côté du vrai problème. Lint mis en filet bloquant (aurait attrapé les deux) + #49 notif ; helper `ExportFileNames` + 3 tests (90 verts) ; fixes VÉRIFIÉS sur émulateur avec l'APK release + données réelles (flux SAF complet : SAVE → fichier + message). Publication v1.3.4. | | 6 sept. 2026 (session v1.3.5) | **Premier diagnostic à distance RÉUSSI via les logs exportés** : les AppLog de la v1.3.4 prouvent export logs `ok=true` (fix #48 ✓ au téléphone), permission agenda accordée (fix manifest ✓) et révèlent le bug racine EXPLICITE : `ensureCalendar` plantait car l'URI sync-adapter n'embarquait pas ACCOUNT_NAME/ACCOUNT_TYPE (§14 #50). En validant sur émulateur, DEUX bugs découverts : delete d'event sans account (#50 bis — « supprimé » logué mais l'event restait) et `buildTreatment` sans `calendarEventId` (#51 — événements orphelins). Cycle ON→save→OFF→save validé end-to-end (`content query`). Publication v1.3.5. | | 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.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. | @@ -407,7 +408,7 @@ Cp(t) = D·k1·k2 · [ e^(−k1·t)/((k1−k2)(k1−k3)) + e^(−k3·t)/((k1−k ## 8. Tests unitaires -**107 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — **89 sans +**112 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — **94 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 = @@ -503,7 +504,7 @@ dépôt (ni maintenant, ni si le repo devient public un jour). seule), plusieurs versions intermédiaires, versions non numériques. - **`AppLogTest`** (4) : v1.3.1 — formatage des lignes horodatées, buffer circulaire (600 → 500 dernières, bordures). -- **`HrtDurationTest`** (5) : v1.3.1 — jours depuis la 1re prise (0 si +- **`HrtDurationTest`** (6) : v1.3.1 — jours depuis la 1re prise (0 si futur/invalide), décomposition mois(30 j)/jours. - **`CalendarRruleTest`** (3) : v1.3.0 — RRULE `FREQ=DAILY;INTERVAL=N`, arrondi demi-supérieur des décimales (6,5 → 7), clamp ≥ 1 jour. @@ -526,6 +527,7 @@ dépôt (ni maintenant, ni si le repo devient public un jour). et 0 au-delà de 10 × t½ (par design) ; `computeCurve(modelOverride=TFS)` sur un traitement stocké ESE reproduit le pic V3C EV 295 — c'est le test du dispatch `concentrationOfDose` + de `cutoffHours`. +- **`ChartZoomTest`** (7) : v1.2.9 (pas d'échantillonnage) + v1.4.1 — **horizon de prévision** 12 × Posologie borné 30 j–1 an (`forecastHorizonHours`, null sans Posologie) et **clamp du panoramique** (`clampPanHours` : panHours négatif = futur, borné par l'horizon ; prévision OFF → futur interdit) — la logique de fenêtre du graphique est centralisée et testée. - **`ReminderScheduleTest`** (8) : v1.4.0 — fix #52 : rappel sur la GRILLE Posologie (dose il y a 2 j + intervalle 7 j → alarme dans 5 j à l'heure choisie, PAS demain), heure déjà passée le jour du créneau → créneau @@ -590,11 +592,20 @@ 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), carte prochaine dose, chips de log rapide + (E2 ≈ X pg/mL, T ≈ Y ng/mL, delta vs 6 h), carte prochaine dose (**v1.4.1 : + delta en JOURS au-delà de 24 h — « 5 j 2 h · sam. 6 18:00 » via + `HrtDuration.daysAndHours` + `next_dose_days` ; en dessous, h/min**), chips de log rapide (+ FAB), mini-chart 24 h (multi-séries via `ChartSeries`) **cliquable → écran Graphiques** (v1.2.1) avec mini-légende E2/T ; données auto-calibrées si l'option est active (`scalePerEster = effectiveAuto?.esterScales`) ; rafraîchissement `tick` 60 s -- `ChartScreen` (v1.2, le plus riche) : plages 24 h/7 j/30 j + **boutons zoom − / +** +- `ChartScreen` (v1.2, le plus riche) : **fenêtre temporelle v1.4.1** — + `endMs = now − panHours` avec `panHours` signé : > 0 = passé (tirer vers la + droite), **< 0 = futur** (tirer vers la gauche, uniquement avec la + prévision active, borné par `forecastHorizonHours` = 12 × Posologie borné + 30 j–1 an). **Activer le chip Prévision ne déplace PAS la fenêtre** (la + projection s'étend au-delà et se parcourt au drag) ; désactiver pendant un + voyage futur → retour à maintenant ; clamp centralisé dans + `clampPanHours` (testé). Plages 24 h/7 j/30 j + **boutons zoom − / +** (v1.2.9) ; **panoramique** (`detectHorizontalDragGestures` — tirer vers la droite remonte dans le passé, `panHours` borné à [0, âge de la 1ʳᵉ dose + plage], bouton « Revenir à maintenant ») ; @@ -1147,8 +1158,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` — 107 au - total, 89 si `local-test-data/` est absent (les 3 classes de régression +2. **Tests verts obligatoires** : `./gradlew testDebugUnitTest` — 112 au + total, 94 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 : @@ -1418,14 +1429,22 @@ Sur le téléphone de test (à compléter par l'utilisatrice) : pg/mL/mg) et diverger en queues (V3C = queues exactes) ; vérifier un preset EB/EC/PEP (nouvelles courbes) ; PEP à 32,5 mg → niveaux comparables à un EEn à 5 mg +- [ ] **v1.4.1** : **prévision sans saut** : ouvrir Graphiques → activer le + chip Prévision → le graphique NE BOUGE PAS (avant v1.4.1 il sautait + « tout à droite ») ; tirer vers la GAUCHE → la courbe projetée défile + vers le futur (jusqu'à 1 an selon la Posologie) ; « Revenir à + maintenant » ramène ; désactiver le chip dans le futur → retour auto +- [ ] **v1.4.1** : **carte « Prochaine dose » en jours** : avec un créneau + à > 24 h, l'accueil affiche « 5 j 2 h · sam. 6 18:00 (traitement) » + (et non « 122h22 ») ; à < 24 h, format heures/minutes inchangé --- -*Doc mise à jour le 6 sept. 2026 (v1.4.0) — build OK, lint vert, 107/107 tests verts (89 sans +*Doc mise à jour le 6 sept. 2026 (v1.4.1) — build OK, lint vert, 112/112 tests verts (94 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). 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. Dette connue : fragments de labs dans l'historique git (v1.1.0→v1.2.3) — cf §8.bis.* diff --git a/docs/GUIDE_INSTALLATION.md b/docs/GUIDE_INSTALLATION.md index 1fd8cd6..899a626 100644 --- a/docs/GUIDE_INSTALLATION.md +++ b/docs/GUIDE_INSTALLATION.md @@ -101,6 +101,8 @@ Les notifications de l'app remontent automatiquement sur la montre via **Huawei - Ester (EV / EU / EEn) + modèle (Estrannaise / Transfem Science) = les courbes du `.ods` - Pour gel/patch/oral : paramètres Bateman (temps au pic, demi-vie, biodispo) 2. **Doses → +** → logue tes injections passées + - L'accueil affiche la **prochaine dose en jours** quand elle est à plus de 24 h + (ex. « 5 j 2 h · sam. 6 18:00 »), en heures/minutes sinon - Astuce mise à jour : l'app affiche les **nouveautés** de chaque version au démarrage (fermable) ; la **version installée** et le **lien des releases** sont dans Paramètres (date/heure exactes, dose en mg) @@ -164,7 +166,11 @@ Pics de référence (dose unique de 5 mg, IM) : simultanément (Estrannaise = bleu, Transfem Science = turquoise) pour comparer - **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) + 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 - **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