v1.4.10 : fix #62 — pan mort sur la vue 24 h : les deltas de doigt (< 1 h) étaient tronqués à 0 à chaque événement → cumul fractionnaire (panDeltaHours, pur + 4 tests : cumul 24 h, 7 j, négatifs, dégénérés) ; limitation de test documentée (§16.ter : l'émulateur headless ne délivre pas les moves intermédiaires au chart — pan non testable par adb, validation sur Pixel 9) ; retrait de l'instrumentation de diagnostic ; 172 tests verts + lint

This commit is contained in:
Siphonight 2026-09-07 19:28:41 +02:00
parent 4045614f3b
commit 3e4c7c44b3
8 changed files with 172 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.9 — build Android ✅, **lint vert** ✅, **168 tests unitaires** ✅ (146 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.10 — build Android ✅, **lint vert** ✅, **172 tests unitaires** ✅ (150 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)
@ -93,7 +93,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 # 168 tests (146 sans les données locales)
./gradlew testDebugUnitTest # 172 tests (150 sans les données locales)
./gradlew lint # lint vert obligatoire avant release
```
@ -105,7 +105,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.9`) et **deux APK par release** (depuis v1.2.5)
**releases taguées** (`v1.1.0` → `v1.4.10`) 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 = 29
versionName = "1.4.9"
versionCode = 30
versionName = "1.4.10"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

View File

@ -98,6 +98,38 @@ fun xLabelTicks(
return out
}
/**
* Convertit un delta de drag en heures de pan, en CUMULANT le résiduel
* fractionnaire (v1.4.10, fix #62).
*
* ⚠️ BUG #62 : l'ancien code tronquait CHAQUE delta incrémental en heures
* entières (`(pan.x / largeur * plage).toLong()`) — à la plage 24 h, le
* delta d'un doigt réel (~30 px ≈ 0,67 h) était tronqué à 0 → le pan ne
* bougeait JAMAIS sur la vue 24 h (alors qu'il marchait sur 7 j+ : le même
* delta ≈ 4,7 h). Le fix : le résiduel fractionnaire (la part < 1 h) est
* conservé d'un événement de mouvement au suivant — le pan se déclenche dès
* que le CUMUL franchit une heure entière.
*
* Pur → testable en JVM (`ChartZoomTest`).
*
* @param panPx delta horizontal du geste (px, signé).
* @param chartWidthPx largeur du canvas en px.
* @param rangeHours plage temporelle courante (24 h / 7 j / 30 j…).
* @param residualHours résiduel fractionnaire précédent (à ré-injecter).
* @return Pair(heures entières à appliquer au pan, nouveau résiduel).
*/
fun panDeltaHours(
panPx: Float,
chartWidthPx: Int,
rangeHours: Long,
residualHours: Double
): Pair<Long, Double> {
if (chartWidthPx <= 0 || rangeHours <= 0) return 0L to residualHours
val total = panPx / chartWidthPx * rangeHours + residualHours
val whole = total.toLong() // troncature vers ZÉRO (gère les deux signes)
return whole to (total - whole)
}
/** Arrondit une valeur max à une borne « propre » (1/2/2.5/5/10 × 10ⁿ) pour l'axe. */
fun niceCeil(value: Double): Double {
if (value <= 0) return 100.0

View File

@ -225,6 +225,10 @@ fun ChartScreen() {
val gestureFutureHorizon by rememberUpdatedState(futurePanHorizon)
val gestureMaxPan by rememberUpdatedState(maxPanHours)
val gestureExtension by rememberUpdatedState(forecastExtensionH)
// Résiduel fractionnaire du pan (fix #62) : conserve la part < 1 h des
// deltas pour que le pan fonctionne sur la vue 24 h (les deltas de doigt
// y sont inférieurs à une heure entière)
var panResidualHours by remember { mutableStateOf(0.0) }
// Doses PRÉVISIONNELLES (v1.4.5) : les mêmes que la courbe utilise —
// exposées pour être MARQUÉES sur le graphique (lignes pointillées aux
@ -330,13 +334,15 @@ fun ChartScreen() {
}
}
// v1.4.6 : l'écran DÉFILIE verticalement — la rangée des toggles de
// modèles (Estrannaise/Transfem/WHSAH) était coupée sous le pli sur les
// écrans standards (le chip WHSAH était littéralement hors écran).
// ⚠️ PAS de verticalScroll ici (fix #62) : le scroll parent AVALE le
// drag horizontal du graphique (pan + pinch, cf §11) — le pan était
// totalement mort avec lui (vu en émulateur v1.4.9). Le contenu tient
// sur les écrans standards (chips en 3 rangées compactes + carte 320 dp,
// vérifié par dump §16.ter) ; si un futur ajout repousse la carte sous
// le pli : compacter les rangées plutôt que re-rajouter un scroll.
Column(
Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(16.dp)
) {
TopAppBar(
@ -450,7 +456,13 @@ fun ChartScreen() {
// 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()
// v1.4.10 (fix #62) : cumul fractionnaire du pan
// (l'ancienne troncature par delta tuait le pan
// sur la vue 24 h)
val (hoursDragged, newResidual) = com.hormonetrack.ui.components.panDeltaHours(
pan.x, chartWidthPx, rangeHours, panResidualHours
)
panResidualHours = newResidual
if (hoursDragged != 0L) {
panHours = com.hormonetrack.ui.components.clampPanHours(
panHours + hoursDragged, gestureFutureHorizon, gestureMaxPan

View File

@ -152,6 +152,58 @@ class ChartZoomTest {
rangeHours = 6L, horizonHours = 24L, firstSlotGapHours = 0L))
}
// ── v1.4.10 (fix #62) : pan en cumul fractionnaire (la vue 24 h ne
// bougeait jamais : chaque delta de doigt < 1 h était tronqué à 0) ──
@Test
fun `pan at 24h accumulates fractional deltas until one full hour is reached`() {
// doigt réel à 24 h : deltas de ~30 px ≈ 0,67 h → tronqués à 0 AVANT
// le fix (le pan ne bougeait jamais) ; le cumul déclenche à 2 deltas
var residual = 0.0
var applied = 0L
for (i in 1..2) {
val (h, r) = com.hormonetrack.ui.components.panDeltaHours(
panPx = 30f, chartWidthPx = 1080, rangeHours = 24L, residualHours = residual
)
applied += h; residual = r
}
assertEquals(1L, applied)
assertEquals(0.34, residual, 0.01)
}
@Test
fun `pan at 7 days moves on the first delta (same finger, wider range)`() {
// le MÊME delta de 30 px à 7 j = 4,67 h → 4 h appliqués immédiatement
val (h, r) = com.hormonetrack.ui.components.panDeltaHours(
panPx = 30f, chartWidthPx = 1080, rangeHours = 24L * 7, residualHours = 0.0
)
assertEquals(4L, h)
assertEquals(0.67, r, 0.01)
}
@Test
fun `pan handles negative deltas (drag toward the past) with fractional accumulation`() {
var residual = 0.0
var applied = 0L
for (i in 1..3) {
val (h, r) = com.hormonetrack.ui.components.panDeltaHours(
panPx = -20f, chartWidthPx = 1080, rangeHours = 24L, residualHours = residual
)
applied += h; residual = r
}
// −0,44 −0,44 → −0,89 (troncature vers 0 = 0) puis −1,33 → −1
assertEquals(-1L, applied)
assertEquals(-0.33, residual, 0.01)
}
@Test
fun `pan helper degenerate inputs`() {
assertEquals(0L to 0.0, com.hormonetrack.ui.components.panDeltaHours(
panPx = 100f, chartWidthPx = 0, rangeHours = 24L, residualHours = 0.0))
assertEquals(0L to 0.0, com.hormonetrack.ui.components.panDeltaHours(
panPx = 100f, chartWidthPx = 1080, rangeHours = 0L, residualHours = 0.0))
}
// ── v1.4.7 (fix #58) : toggles de modèles alignés sur les traitements ──
@Test

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.9`, …).
Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.4.10`, …).
## [1.4.10] — 2026-09-07 (versionCode 30)
### Corrigé
- **Le pan du graphique ne fonctionnait pas sur la vue 24 h** (#62, remontée
par test utilisateur sur Pixel 9 : « le glisser horizontal ne marche pas
sur une échelle de 24 h, uniquement au-dessus ») : les deltas de mouvement
du doigt (~30 px ≈ 0,67 h à 24 h) étaient **tronqués à 0** par le cast
en heures entières à CHAQUE événement de mouvement → le pan ne bougeait
jamais sur 24 h (alors que sur 7 j le même delta ≈ 4,7 h). FIX : **cumul
fractionnaire** — le résiduel < 1 h est conservé d'un événement au
suivant (`panDeltaHours`, pur et testé) : le pan se déclenche dès que le
cumul franchit une heure entière. Aucun impact sur les vues larges.
- **Nettoyage** : instrumentation de diagnostic retirée ; 2 imports/
constantes morts supprimés.
### Ajouté
- **4 nouveaux tests** (172 au total / 150 sans les données locales) :
cumul fractionnaire à 24 h (2 deltas → 1 h), 7 j (delta immédiat),
deltas négatifs (troncature vers zéro), cas dégénérés.
- **Limitation de test documentée** (§16.ter) : l'émulateur headless ne
délivre pas les mouvements intermédiaires au graphique via `adb input
swipe` → le pan n'est PAS testable par adb sur cet environnement ; le
test humain sur Pixel 9 reste la référence pour les gestes.
## [1.4.9] — 2026-09-07 (versionCode 29)
## [1.4.9] — 2026-09-07 (versionCode 29)

View File

@ -92,6 +92,7 @@ Fonctionnalités v1 :
| 7 sept. 2026 (session v1.4.7) | **Toggles alignés sur les traitements** (#58, demande) : au chargement, seuls les modèles utilisés par des traitements à PROFIL PK (`usesProfileModel`) sont ON (l'oral/AA polluait le set — vu en émulateur avec le CPA) ; garde-fou TFS si aucun profil ; une seule init. **Épinglé : la calibration s'applique à TOUTES les courbes** (par ESTER via doseEster, indépendant du modelOverride — test). 158 verts. Publication v1.4.7. | + **#59** : le chip de la carte de traitement affichait « Estrannaise » pour WHSAH (ternaire à 2 branches → `modelLabelRes` centralisé).
| 7 sept. 2026 (session v1.4.8) | **Calibration PAR MODÈLE PK** (#60, remontée : « ×2,21 sur WHSAH alors que l'idéal serait < 1 ») : un seul jeu de facteurs (calculé depuis le modèle stocké) était partagé par toutes les courbes → absurde dès qu'un traitement basculait de modèle. FIX : le graphique calcule une AutoCalibrated PAR modèle affiché (ESE/TFS/WHS — modelOverride propagé à computeEsterScaleFactors et computeTKPerEster) → chaque courbe calibrée passe par les labs. Home inchangé (modèle stocké). Test : chaque courbe calibrée ≈ lab (±2 %), échelles TFS ≠ WHS. 159 verts. Publication v1.4.8. |
| 7 sept. 2026 (session v1.4.9) | **Fix #61 — calibration tombant à ×2,21 (auto ET manuelle)** : reproduction par test sur données réelles — une dose EEn de test de JANVIER (10 mg) dans l'export, des labs de janvier-mars à 15–76 j → prédiction WHSAH résiduelle (184 → 1) → ratios aberrants (×3,3 → ×391) → médiane ×2,21, dans les DEUX pipelines. FIX : garde de significativité `labIsSignificant` (prédiction ≥ 15 % du max observé, labs par timestamp croissant) sur les 3 pipelines ; facteur WHSAH retombe à 0,55. + Hint UX (auto ON → facteur manuel ignoré à l'affichage) + nettoyage CHANGELOG (4 titres orphelins doublés [1.4.4–1.4.7]). 168 verts. Publication v1.4.9. |
| 7 sept. 2026 (session v1.4.10) | **Pan mort sur la vue 24 h** (#62, remontée par test utilisateur Pixel 9 : « le glisser horizontal ne marche pas sur 24 h, uniquement au-dessus ») : les deltas de doigt (~30 px ≈ 0,67 h à 24 h) étaient tronqués à 0 par le cast entier à CHAQUE événement de mouvement → le pan ne bougeait jamais sur 24 h. FIX : cumul fractionnaire (`panDeltaHours` — pur, testé : 2 deltas de 0,67 h → 1 h). **Subtilité de test documentée (§16.ter)** : l'émulateur headless ne délivre pas les mouvements intermédiaires au chart via `adb input swipe` → le pan n'est PAS testable par adb (l'utilisateur a validé sur Pixel 9). 172 verts. Publication v1.4.10. |
| 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. |
@ -467,7 +468,7 @@ niveau que les deux autres, pas de remplacement »).
## 8. Tests unitaires
**168 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — **146 sans
**172 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — **150 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 =
@ -603,6 +604,10 @@ dépôt (ni maintenant, ni si le repo devient public un jour).
- **`WhsahEngineTest`** (6) : + v1.4.8 — **calibration PAR MODÈLE** (fix
#60) : chaque courbe calibrée avec la prédiction de SON modèle passe par
le lab (±2 %) ; échelles TFS ≠ WHS au même lab.
- **`ChartZoomTest`** (19) : + v1.4.10 — **pan en cumul fractionnaire**
(`panDeltaHours`, fix #62 : les deltas de doigt < 1 h s'accumulent
jusqu'à franchir une heure entière — la vue 24 h ne bougeait jamais ;
deltas négatifs = troncature vers zéro, cas dégénérés).
- **`ScaleFactorWhsahReproTest`** (4) : v1.4.9 — épinglé sur le bug ×2,21
(#61) : facteur manuel WHSAH 0,55 (plus jamais 2,21), cohérence
auto↔manuel (±15 %), garde de significativité (séquence réelle 184→1 et
@ -1173,6 +1178,20 @@ l'exemple) :**
**Session v1.4.0 (modèle TFS sur la méta-analyse + fix #52) :**
62. **Pan mort sur la vue 24 h** (v1.4.10, remontée par test utilisateur
Pixel 9 : « le glisser horizontal ne marche pas sur 24 h, uniquement
au-dessus ») : le pan incrémental tronquait CHAQUE delta en heures
entières (`(pan.x / largeur * plage).toLong()`) — à 24 h, le delta
d'un doigt réel (~30 px ≈ 0,67 h) tombait à 0 à CHAQUE événement →
le pan ne bougeait jamais sur 24 h (sur 7 j : le même delta = 4,7 h).
FIX : cumul fractionnaire (`panDeltaHours` — le résiduel < 1 h est
conservé d'un événement au suivant, pur et testé). Leçon : un cast en
entiers par delta incrémental tue les mouvements fins — cumuler le
résiduel fractionnaire. ⚠️ Le pan n'est PAS testable par adb sur
l'émulateur headless (les `input swipe` ne délivrent pas les
mouvements intermédiaires au chart) — le test humain sur Pixel 9
reste la référence pour les gestes (§16.ter).
61. **Calibration tombant à ×2,21 (auto ET manuelle)** (v1.4.9, remontée
« le bouton Calibrer avec les analyses du traitement EEn WHSAH me met
toujours à ×2,21 ») : reproduction par test sur données réelles —
@ -1415,8 +1434,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` — 168 au
total, 146 si `local-test-data/` est absent (les 3 classes de régression
2. **Tests verts obligatoires** : `./gradlew testDebugUnitTest` — 172 au
total, 150 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 :
@ -1516,6 +1535,14 @@ adb shell input keyevent KEYCODE_BACK
signal ; stack : `adb logcat -b crash -d` (grep FATAL). **0 crash attendu :
`grep -c FATAL` reste à 0** après chaque interaction.
**4.bis LIMITATION des gestes dans l'émulateur headless (v1.4.10)** : les
`adb shell input swipe` ne délivrent PAS les mouvements intermédiaires au
chart (`detectTransformGestures` ne reçoit que down/up sans moves) → le
pan et le pinch sont INTESTABLES par adb sur cet environnement (bug #62
non détectable ici). Le pan/pinch se valide sur le Pixel 9 réel (l'appa-
rel du test humain). Les TAPS, la navigation et les dialogs fonctionnent
normalement par adb.
**5. Flux SAF (export JSON/logs)** : sur android-31 ATD le
DocumentsUI est STUBBÉ (`fakesystemapp`) → INUTILISABLE ; utiliser l'image
android-36 google_apis. Le picker se pilote pareil (bouton `SAVE` en bas à
@ -1719,6 +1746,11 @@ Sur le téléphone de test (à compléter par l'utilisatrice) :
- [ ] **v1.4.6** : **3ᵉ modèle WHSAH** : preset WHSAH → courbe verte
superposable aux 2 autres (montée plus rapide à J+1, t½ plus longues
que TFS) ; PEP non couvert ; 6 presets ; dropdown modèle à 3 choix
- [ ] **v1.4.10** : **pan sur la vue 24 h** : glisser horizontalement à
24 h doit déplacer la courbe (fix #62 : les deltas < 1 h s'accumulent
— avant, le pan ne bougeait jamais sur 24 h) ; tester aussi à 7 j et
le drag gauche avec prévision (futur)
- [ ] **v1.4.9** : **calibration ×2,21 corrigée** (#61) : le bouton
« Calibrer avec les analyses » et l'auto-calibration retombent sur un
facteur physiologique (< 1,2) même avec des doses de test anciennes ;
@ -1734,7 +1766,7 @@ Sur le téléphone de test (à compléter par l'utilisatrice) :
tap ; et TOUTES les courbes affichées reflètent la calibration (test)
---
*Doc mise à jour le 7 sept. 2026 (v1.4.9) — build OK, lint vert, 168/168 tests verts (146 sans
*Doc mise à jour le 7 sept. 2026 (v1.4.10) — build OK, lint vert, 172/172 tests verts (150 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

View File

@ -191,6 +191,9 @@ Pics de référence (dose unique de 5 mg, IM) :
graphique, seuls les modèles utilisés par tes traitements (injections, en
cours ou passés) sont affichés — les autres s'activent au tap ; et TOUTES
les courbes affichées suivent ta calibration (labs), quel que soit le modèle
- **Pan fluide sur la vue 24 h** (v1.4.10) : le glisser horizontal fonctionne
sur toutes les échelles, y compris 24 h (les petits mouvements de doigt
s'accumulent jusqu'à franchir une heure entière)
- **Marqueurs de doses** (v1.4.5) : les doses projetées sont marquées par une
ligne verticale pointillée avec leur heure ; tes prises enregistrées par un
petit triangle discret au bas du graphique — plus d'ambiguïté sur « où » la