v1.2.9 : zoom du graphique (pinch + boutons, 6 h → 300 j, focal stable, pas adaptatif) + disclaimer IA dans le README

- ChartScreen : detectTransformGestures unique (pan 1 doigt + pinch 2 doigts,
  pas de conflit de consommation) ; zoom autour du point focal (le temps sous
  le doigt reste fixe) ; plage bornée 6 h → 300 j (MIN/MAX_RANGE_H) ;
  boutons − / + en accessibilité
- CurveChart : stepForRange (15 min ≤ 12 h, 30 min ≤ 24 h, sinon 1 h) passé à
  computeCurve (courbes lisses à fort zoom) ; labels X adaptatifs 1 h/3 h
- README : disclaimer IA en en-tête (développé avec l'IA, contribution
  humaine essentielle = feedback/retours/suggestions, renvoi §2 doc dev)
  ; DEVELOPPEMENT §1 mention IA-assistée, §11 zoom, §8 67 tests
- ChartZoomTest : 3 tests stepForRange — 67 tests verts
- versionCode 12, versionName 1.2.9
This commit is contained in:
Siphonight 2026-09-05 22:51:27 +02:00
parent 1249dcdc79
commit 4b9d5904a8
9 changed files with 151 additions and 16 deletions

View File

@ -1,5 +1,12 @@
# HormoneTrack # HormoneTrack
> **🤖 Développé avec l'IA** : ce projet a été conçu et codé avec un assistant IA.
> La contribution humaine a été **essentielle** : feedback continu, retours
> utilisateur (tests sur vrai téléphone, rapports de bugs accompagnés d'exports
> réels), suggestions d'améliorations et validation de chaque release. Le
> détail session par session est documenté dans
> [docs/DEVELOPPEMENT.md §2](docs/DEVELOPPEMENT.md).
Suivi de thérapie hormonale (THS) sur Android, avec courbes estimées **heure par heure** Suivi de thérapie hormonale (THS) sur Android, avec courbes estimées **heure par heure**
d'estradiol (E2) et de testostérone (T), calibration sur les prises de sang, rappels d'estradiol (E2) et de testostérone (T), calibration sur les prises de sang, rappels
affichés sur smartwatch (Huawei Watch GT 3 via Gadgetbridge ou Huawei Health) et affichés sur smartwatch (Huawei Watch GT 3 via Gadgetbridge ou Huawei Health) et
@ -9,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 > à titre informatif — ce ne sont pas des mesures. Fie-toi toujours à tes analyses de
> sang et aux consignes de ton endocrinologue. > sang et aux consignes de ton endocrinologue.
- **Statut** : v1.2.8 — build Android ✅, **64 tests unitaires** ✅ (44 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.2.9 — build Android ✅, **67 tests unitaires** ✅ (44 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) - **Journal des versions** : [docs/CHANGELOG.md](docs/CHANGELOG.md)
- **Guide utilisateur** : [docs/GUIDE_INSTALLATION.md](docs/GUIDE_INSTALLATION.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) - **Doc de développement** (architecture, maths, décisions, bugs) : [docs/DEVELOPPEMENT.md](docs/DEVELOPPEMENT.md)
@ -18,8 +25,9 @@ sauvegarde JSON. **100 % local, aucun compte, aucun serveur.**
## Fonctionnalités ## Fonctionnalités
- **Courbes estimées heure par heure** : E2 (pg/mL) et T (ng/mL), vue 24 h / 7 j / 30 j, - **Courbes estimées heure par heure** : E2 (pg/mL) et T (ng/mL), vue 24 h / 7 j / 30 j,
**panoramique** (glisser pour remonter dans le passé), **pics & creux** affichables **panoramique** (glisser pour remonter dans le passé), **zoom** (pinch ou boutons
(triangles ▲▼ aux extrema locaux, toggle) − / +, 6 h → 300 j, échantillonnage adaptatif), **pics & creux** affichables avec
leurs **valeurs estimées** (triangles ▲▼ aux extrema locaux, toggle)
- **Deux modèles PK au choix, superposables** (issus de la feuille `Estrogen.ods`) : - **Deux modèles PK au choix, superposables** (issus de la feuille `Estrogen.ods`) :
**Estrannaise (EstraNase)** et **Transfem Science** pour les injections EV / EU / EEn, **Estrannaise (EstraNase)** et **Transfem Science** pour les injections EV / EU / EEn,
affichés côte à côte avec toggles indépendants affichés côte à côte avec toggles indépendants

View File

@ -12,8 +12,8 @@ android {
applicationId = "com.hormonetrack" applicationId = "com.hormonetrack"
minSdk = 26 minSdk = 26
targetSdk = 36 targetSdk = 36
versionCode = 11 versionCode = 12
versionName = "1.2.8" versionName = "1.2.9"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {

View File

@ -66,6 +66,17 @@ fun niceCeil(value: Double): Double {
return nice * exp return nice * exp
} }
/**
* Pas d'échantillonnage de la courbe selon la plage affichée (v1.2.9) :
* plus on zoome, plus le pas finit pour des courbes lisses
* (≤ 12 h → 15 min ; ≤ 24 h → 30 min ; sinon 1 h).
*/
fun stepForRange(rangeHours: Long): Long = when {
rangeHours <= 12L -> 900_000L // 15 min
rangeHours <= 24L -> 1_800_000L // 30 min
else -> 3_600_000L // 1 h
}
/** /**
* Graphique Canvas pur, multi-séries : * Graphique Canvas pur, multi-séries :
* - axe gauche = E2 (pg/mL), axe droit = T (ng/mL) ; * - axe gauche = E2 (pg/mL), axe droit = T (ng/mL) ;
@ -309,7 +320,10 @@ private fun DrawScope.drawXLabels(
) { ) {
val paint = labelPaint(android.graphics.Paint.Align.CENTER) val paint = labelPaint(android.graphics.Paint.Align.CENTER)
val spanH = (t1 - t0) / 3_600_000.0 val spanH = (t1 - t0) / 3_600_000.0
// Pas des labels X adaptatif — le zoom (v1.2.9) descend jusqu'à quelques heures
val stepHours = when { val stepHours = when {
spanH <= 8 -> 1
spanH <= 18 -> 3
spanH <= 25 -> 6 spanH <= 25 -> 6
spanH <= 25 * 7 -> 24 spanH <= 25 * 7 -> 24
else -> 24 * 5 else -> 24 * 5

View File

@ -1,6 +1,6 @@
package com.hormonetrack.ui.screens package com.hormonetrack.ui.screens
import androidx.compose.foundation.gestures.detectHorizontalDragGestures import androidx.compose.foundation.gestures.detectTransformGestures
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -9,12 +9,18 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Remove
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.Icon
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -51,6 +57,9 @@ import kotlinx.coroutines.withContext
private const val HOUR_MS = 3_600_000L private const val HOUR_MS = 3_600_000L
private const val DAY_MS = 24 * HOUR_MS private const val DAY_MS = 24 * HOUR_MS
// Zoom (v1.2.9) : plage temporelle bornée 6 h → 300 j
private const val MIN_RANGE_H = 6L
private const val MAX_RANGE_H = 24L * 300
/** /**
* Écran Graphiques. * Écran Graphiques.
@ -79,6 +88,7 @@ fun ChartScreen() {
val tConfig by container.settings.tConfig.collectAsState(initial = TConfig()) val tConfig by container.settings.tConfig.collectAsState(initial = TConfig())
val autoCalibrate by container.settings.autoCalibrate.collectAsState(initial = false) val autoCalibrate by container.settings.autoCalibrate.collectAsState(initial = false)
// Zoom temporel (v1.2.9) : plage bornée 6 h → 300 j, pinch + boutons
var rangeHours by remember { mutableLongStateOf(24L) } var rangeHours by remember { mutableLongStateOf(24L) }
var panHours by remember { mutableLongStateOf(0L) } var panHours by remember { mutableLongStateOf(0L) }
var showT by remember { mutableStateOf(true) } var showT by remember { mutableStateOf(true) }
@ -144,10 +154,13 @@ fun ChartScreen() {
} }
} else emptyList() } else emptyList()
val allDoses = doseLogs + forecastDoses val allDoses = doseLogs + forecastDoses
// Pas d'échantillonnage adaptatif au zoom (v1.2.9)
val step = com.hormonetrack.ui.components.stepForRange(rangeHours)
if (showEse) { if (showEse) {
list.add( list.add(
"ESE" to PharmacokineticEngine.computeCurve( "ESE" to PharmacokineticEngine.computeCurve(
effTreatments, allDoses, startMs, endMs, effTreatments, allDoses, startMs, endMs,
stepMs = step,
tConfig = effTConfig, modelOverride = "ESE", tConfig = effTConfig, modelOverride = "ESE",
scalePerEster = effScales, scalePerEster = effScales,
tKPerEster = effectiveAuto?.tKPerEster tKPerEster = effectiveAuto?.tKPerEster
@ -158,6 +171,7 @@ fun ChartScreen() {
list.add( list.add(
"TFS" to PharmacokineticEngine.computeCurve( "TFS" to PharmacokineticEngine.computeCurve(
effTreatments, allDoses, startMs, endMs, effTreatments, allDoses, startMs, endMs,
stepMs = step,
tConfig = effTConfig, modelOverride = "TFS", tConfig = effTConfig, modelOverride = "TFS",
scalePerEster = effScales, scalePerEster = effScales,
tKPerEster = effectiveAuto?.tKPerEster tKPerEster = effectiveAuto?.tKPerEster
@ -217,6 +231,20 @@ fun ChartScreen() {
onClick = { rangeHours = 24L * 30 }, onClick = { rangeHours = 24L * 30 },
label = { Text(stringResource(R.string.chart_30j)) } label = { Text(stringResource(R.string.chart_30j)) }
) )
Spacer(Modifier.width(4.dp))
// Zoom manuel (accessibilité — le pinch fonctionne aussi)
FilledTonalIconButton(
onClick = { rangeHours = (rangeHours * 2).coerceAtMost(MAX_RANGE_H) },
enabled = rangeHours < MAX_RANGE_H
) {
Icon(Icons.Filled.Add, contentDescription = stringResource(R.string.zoom_in))
}
FilledTonalIconButton(
onClick = { rangeHours = (rangeHours / 2).coerceIn(MIN_RANGE_H, MAX_RANGE_H) },
enabled = rangeHours > MIN_RANGE_H
) {
Icon(Icons.Filled.Remove, contentDescription = stringResource(R.string.zoom_out))
}
} }
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
@ -268,15 +296,33 @@ fun ChartScreen() {
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.onSizeChanged { chartWidthPx = it.width } .onSizeChanged { chartWidthPx = it.width }
.pointerInput(rangeHours, maxPanHours) { .pointerInput(Unit) {
// Glisser vers la droite = remonter dans le passé // Pan (1 doigt) + pinch-zoom (2 doigts, v1.2.9) — un seul
detectHorizontalDragGestures { change, dragAmount -> // handler pour éviter les conflits de consommation
change.consume() detectTransformGestures { centroid, pan, zoom, _ ->
if (chartWidthPx > 0 && maxPanHours > 0) { val forecastMs =
val hoursDragged = (dragAmount / chartWidthPx * rangeHours).toLong() (if (showForecast) (forecastHorizonHours ?: 0L) * HOUR_MS else 0L)
if (chartWidthPx > 0) {
// Pan horizontal : glisser à droite = remonter le temps
val hoursDragged = (pan.x / chartWidthPx * rangeHours).toLong()
if (hoursDragged != 0L) { if (hoursDragged != 0L) {
panHours = (panHours + hoursDragged).coerceIn(0L, maxPanHours) panHours = (panHours + hoursDragged).coerceIn(0L, maxPanHours)
} }
// Zoom autour du point focal : le temps sous le doigt
// reste fixe pendant le pinch
if (zoom != 1f && zoom > 0f) {
val f = (centroid.x / chartWidthPx.toDouble()).coerceIn(0.0, 1.0)
val newRange = (rangeHours / zoom).toLong()
.coerceIn(MIN_RANGE_H, MAX_RANGE_H)
if (newRange != rangeHours) {
val endAnchor = nowMs + forecastMs - 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)
rangeHours = newRange
}
}
} }
} }
} }

View File

@ -31,6 +31,8 @@
<string name="show_labs">Analyses</string> <string name="show_labs">Analyses</string>
<string name="show_forecast">Prévision</string> <string name="show_forecast">Prévision</string>
<string name="show_extrema">Pics / creux</string> <string name="show_extrema">Pics / creux</string>
<string name="zoom_in">Zoom avant (fenêtre plus courte)</string>
<string name="zoom_out">Zoom arrière (fenêtre plus longue)</string>
<string name="legend_extrema">▲▼ pics &amp; creux estimés (par courbe)</string> <string name="legend_extrema">▲▼ pics &amp; creux estimés (par courbe)</string>
<string name="back_to_present">Revenir à maintenant</string> <string name="back_to_present">Revenir à maintenant</string>
<string name="drag_hint">Fais glisser le graphique vers la droite pour remonter dans le passé.</string> <string name="drag_hint">Fais glisser le graphique vers la droite pour remonter dans le passé.</string>

View File

@ -31,6 +31,8 @@
<string name="show_labs">Labs</string> <string name="show_labs">Labs</string>
<string name="show_forecast">Forecast</string> <string name="show_forecast">Forecast</string>
<string name="show_extrema">Peaks / troughs</string> <string name="show_extrema">Peaks / troughs</string>
<string name="zoom_in">Zoom in (shorter window)</string>
<string name="zoom_out">Zoom out (longer window)</string>
<string name="legend_extrema">▲▼ estimated peaks &amp; troughs (per curve)</string> <string name="legend_extrema">▲▼ estimated peaks &amp; troughs (per curve)</string>
<string name="back_to_present">Back to now</string> <string name="back_to_present">Back to now</string>
<string name="drag_hint">Drag the chart to the right to travel into the past.</string> <string name="drag_hint">Drag the chart to the right to travel into the past.</string>

View File

@ -0,0 +1,31 @@
package com.hormonetrack.ui
import com.hormonetrack.ui.components.stepForRange
import org.junit.Assert.assertEquals
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.
*/
class ChartZoomTest {
@Test
fun `deep zoom uses 15 minute steps`() {
assertEquals(900_000L, stepForRange(6L))
assertEquals(900_000L, stepForRange(12L))
}
@Test
fun `mid zoom uses 30 minute steps`() {
assertEquals(1_800_000L, stepForRange(13L))
assertEquals(1_800_000L, stepForRange(24L))
}
@Test
fun `wide ranges use hourly steps`() {
assertEquals(3_600_000L, stepForRange(25L))
assertEquals(3_600_000L, stepForRange(24L * 30))
assertEquals(3_600_000L, stepForRange(24L * 300))
}
}

View File

@ -1,7 +1,21 @@
# Changelog — HormoneTrack # Changelog — HormoneTrack
Format : [Keep a Changelog](https://keepachangelog.com/fr-FR/1.1.0/). Format : [Keep a Changelog](https://keepachangelog.com/fr-FR/1.1.0/).
Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.2.8`, …). Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.2.9`, …).
## [1.2.9] — 2026-09-05 (versionCode 12)
### Ajouté
- **Zoom du graphique de simulation** : pinch à 2 doigts **et** boutons − / +
(accessibilité), plage bornée **6 h → 300 j** ; le point focal du pinch reste
fixe dans le temps ; échantillonnage adaptatif (15 min ≤ 12 h, 30 min ≤
24 h, sinon 1 h — courbes lisses à fort zoom) ; labels X adaptatifs
(1 h / 3 h). Un seul gestionnaire de gestes gère pan + zoom (pas de
conflit de consommation).
- **README : disclaimer IA** en en-tête — projet développé avec l'IA,
contribution humaine essentielle (feedback, retours utilisateur, suggestions) ;
mention factuelle reprise en §1/§2 de la doc de développement.
- 3 tests `stepForRange` (67 au total).
## [1.2.8] — 2026-09-05 (versionCode 11) ## [1.2.8] — 2026-09-05 (versionCode 11)

View File

@ -45,6 +45,11 @@ Android, apps tierces au poignet quasi impossibles (cf §17). Utilisatrice équi
**Gadgetbridge** (FOSS) sur son téléphone → v1 = **app téléphone + notifications miroir **Gadgetbridge** (FOSS) sur son téléphone → v1 = **app téléphone + notifications miroir
sur la montre** via GB (ou Huawei Health). sur la montre** via GB (ou Huawei Health).
> **Développement IA-assisté** : le code a été produit avec un assistant IA ;
> la contribution humaine = feedback continu, retours utilisateur (tests réels
> sur téléphone, bugs avec exports), suggestions et validation des releases
> (détail §2 — factuel, session par session).
Fonctionnalités v1 : Fonctionnalités v1 :
- Courbes estimées heure par heure : E2 (pg/mL) + T (ng/mL) — 24 h / 7 j / 30 j - Courbes estimées heure par heure : E2 (pg/mL) + T (ng/mL) — 24 h / 7 j / 30 j
- Deux modèles PK du `.ods` (Estrannaise / TFS) pour injections EV/EU/EEn ; - Deux modèles PK du `.ods` (Estrannaise / TFS) pour injections EV/EU/EEn ;
@ -71,6 +76,7 @@ Fonctionnalités v1 :
| 5 sept. 2026 (session v1.2.1) | Remontées UX/utilisateur : calibration **par période d'ester** (labs EV → doses EV, labs EEn → doses EEn — corrige les courbes gonflées), lab T "pg/mL" neutralisé, **édition des labs**, tap accueil → Graphiques + mini-légende, auto-cal fusionnée (E2+T, un seul switch) ; 2ᵉ régression épinglée sur le nouvel export ; **44 tests verts**, APK v1.2.1. | | 5 sept. 2026 (session v1.2.1) | Remontées UX/utilisateur : calibration **par période d'ester** (labs EV → doses EV, labs EEn → doses EEn — corrige les courbes gonflées), lab T "pg/mL" neutralisé, **édition des labs**, tap accueil → Graphiques + mini-légende, auto-cal fusionnée (E2+T, un seul switch) ; 2ᵉ régression épinglée sur le nouvel export ; **44 tests verts**, APK v1.2.1. |
| 5 sept. 2026 (session v1.2.2) | Toggle T masque aussi les labs T ; **prise de sang E2 + T en une entrée** (chacune optionnelle) affichée **côte à côte** ; sélecteur d'édition par marqueur ; suppression par prise ; 4 tests de regroupement ; **48 tests verts**, APK v1.2.2. | | 5 sept. 2026 (session v1.2.2) | Toggle T masque aussi les labs T ; **prise de sang E2 + T en une entrée** (chacune optionnelle) affichée **côte à côte** ; sélecteur d'édition par marqueur ; suppression par prise ; 4 tests de regroupement ; **48 tests verts**, APK v1.2.2. |
| 5 sept. 2026 (session v1.2.6) | Valeurs estimées affichées sur les pics/creux (drawExtremum) ; **import JSON en mode écrasement** (bug : conflit d'IDs si données existantes) avec restauration du tConfig et reprogrammation des rappels ; docs ; 62 tests verts, APK v1.2.6 + releases. | | 5 sept. 2026 (session v1.2.6) | Valeurs estimées affichées sur les pics/creux (drawExtremum) ; **import JSON en mode écrasement** (bug : conflit d'IDs si données existantes) avec restauration du tConfig et reprogrammation des rappels ; docs ; 62 tests verts, APK v1.2.6 + 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. |
| 5 sept. 2026 (session v1.2.8) | « Fréquence d'injection » → **« Posologie »** (terme adapté à toutes les voies) ; quirk Gitea découvert (noms d'assets normalisés à l'upload) → renommage PATCH dans le script ; releases avec les 2 APK ; 64 tests verts. | | 5 sept. 2026 (session v1.2.8) | « Fréquence d'injection » → **« Posologie »** (terme adapté à toutes les voies) ; quirk Gitea découvert (noms d'assets normalisés à l'upload) → renommage PATCH dans le script ; releases avec les 2 APK ; 64 tests verts. |
| 5 sept. 2026 (session v1.2.7) | Prévision : les créneaux passés ne sont plus simulés (bug oubli → faux pic historique) ; retard = décalage voulu ; mismatch doc-code §7.3b corrigé ; 2 tests ; **64 tests verts**, APK v1.2.7 + releases. | | 5 sept. 2026 (session v1.2.7) | Prévision : les créneaux passés ne sont plus simulés (bug oubli → faux pic historique) ; retard = décalage voulu ; mismatch doc-code §7.3b corrigé ; 2 tests ; **64 tests verts**, APK v1.2.7 + releases. |
@ -396,7 +402,7 @@ dépôt (ni maintenant, ni si le repo devient public un jour).
- `.gitignore` contient `local-test-data/` → jamais commités ; - `.gitignore` contient `local-test-data/` → jamais commités ;
- les tests font `Assume.assumeTrue(file.exists())` dans le `@Before` : **sans le - les tests font `Assume.assumeTrue(file.exists())` dans le `@Before` : **sans le
fichier, la classe est IGNORÉE** (skipped, pas failed) — un clone neuf ou une CI fichier, la classe est IGNORÉE** (skipped, pas failed) — un clone neuf ou une CI
exécute 44 tests au lieu de 56 ; exécute 44 tests au lieu de 67 ;
- le workdir des tests Gradle est le dossier du module (`app/`) → les tests - le workdir des tests Gradle est le dossier du module (`app/`) → les tests
cherchent les fichiers à plusieurs chemins (`../local-test-data/…` en premier) ; cherchent les fichiers à plusieurs chemins (`../local-test-data/…` en premier) ;
- **pour les lancer** : exporter un backup JSON depuis l'app → l'enregistrer sous - **pour les lancer** : exporter un backup JSON depuis l'app → l'enregistrer sous
@ -455,7 +461,8 @@ Sur la montre : remontée par Gadgetbridge **ou** Huawei Health (cf §17).
(+ FAB), mini-chart 24 h (multi-séries via `ChartSeries`) **cliquable → écran (+ 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 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 active (`scalePerEster = effectiveAuto?.esterScales`) ; rafraîchissement `tick` 60 s
- `ChartScreen` (v1.2, le plus riche) : plages 24 h/7 j/30 j ; **panoramique** - `ChartScreen` (v1.2, le plus riche) : plages 24 h/7 j/30 j + **boutons zoom − / +**
(v1.2.9) ; **panoramique**
(`detectHorizontalDragGestures` — tirer vers la droite remonte dans le passé, (`detectHorizontalDragGestures` — tirer vers la droite remonte dans le passé,
`panHours` borné à [0, âge de la 1ʳᵉ dose + plage], bouton « Revenir à maintenant ») ; `panHours` borné à [0, âge de la 1ʳᵉ dose + plage], bouton « Revenir à maintenant ») ;
**toggles indépendants Estrannaise/TFS** → deux `computeCurve` avec `modelOverride` **toggles indépendants Estrannaise/TFS** → deux `computeCurve` avec `modelOverride`
@ -518,6 +525,15 @@ triangles ▲▼ via `PharmacokineticEngine.detectExtrema` **accompagnés de la
valeur estimée** du pic (au-dessus) ou du creux (en dessous), dans la couleur valeur estimée** du pic (au-dessus) ou du creux (en dessous), dans la couleur
de la série (`drawExtremum`). de la série (`drawExtremum`).
**Zoom (v1.2.9)** : pinch **et** boutons − / + — plage temporelle bornée
**6 h → 300 j** (`MIN_RANGE_H`/`MAX_RANGE_H`), point focal du pinch maintenu
fixe dans le temps (formule centroid), échantillonnage **adaptatif**
(`stepForRange` : 15 min ≤ 12 h, 30 min ≤ 24 h, sinon 1 h — courbes lisses à
fort zoom) et labels X adaptatifs (1 h / 3 h ajoutés). Le pan ET le zoom
partagent un seul `detectTransformGestures` (un handler = pas de conflit de
consommation) ; le zoom/pan restent gérés **par le parent**, le Canvas demeure
purement déclaratif.
Pièges : Pièges :
- `DrawScope` implémente `Density` → `X.dp.toPx()` direct ; ne PAS écrire de helper custom - `DrawScope` implémente `Density` → `X.dp.toPx()` direct ; ne PAS écrire de helper custom
- Tout label passe par `drawContext.canvas.nativeCanvas` + `android.graphics.Paint` - Tout label passe par `drawContext.canvas.nativeCanvas` + `android.graphics.Paint`
@ -918,6 +934,8 @@ Sur le téléphone de test (à compléter par l'utilisatrice) :
- [ ] **v1.2.x** : édition d'une dose (tap ligne Doses) et d'un lab (tap ligne - [ ] **v1.2.x** : édition d'une dose (tap ligne Doses) et d'un lab (tap ligne
Analyses → sélecteur E2/T si paire) ; suppression par prise entière Analyses → sélecteur E2/T si paire) ; suppression par prise entière
- [ ] **v1.2.x** : prise de sang E2 + T en une entrée (champs optionnels) - [ ] **v1.2.x** : prise de sang E2 + T en une entrée (champs optionnels)
- [ ] **v1.2.9** : zoom du graphique (pinch 2 doigts + boutons − / + ; labels
X adaptatifs ; courbes lisses à fort zoom)
- [ ] **v1.2.7** : prévision après un OUBLI (pas de faux pic dans le passé) - [ ] **v1.2.7** : prévision après un OUBLI (pas de faux pic dans le passé)
et après un RETARD (prévision décalée, suivant la dernière prise) et après un RETARD (prévision décalée, suivant la dernière prise)
- [ ] **v1.2.6** : import d'un backup avec l'app déjà remplie → ÉCRASER & - [ ] **v1.2.6** : import d'un backup avec l'app déjà remplie → ÉCRASER &
@ -927,4 +945,4 @@ Sur le téléphone de test (à compléter par l'utilisatrice) :
sur le graphique** et la calibration couvre toujours ses périodes sur le graphique** et la calibration couvre toujours ses périodes
--- ---
*Doc mise à jour le 5 sept. 2026 (v1.2.8) — build OK, 62/62 tests verts (44 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.* *Doc mise à jour le 5 sept. 2026 (v1.2.9) — build OK, 62/62 tests verts (44 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.*