# 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`, …). ## [1.4.9] — 2026-09-07 (versionCode 29) ### Corrigé - **La calibration tombait à ×2,21 (auto ET manuelle) sur un traitement basculé sous un autre modèle** (#61, remontée : « le bouton Calibrer avec les analyses du traitement EEn WHSAH me met toujours à ×2,21 ») : diagnostic par reproduction sur données réelles — l'export contient une dose EEn de test logguée en **janvier** ; les labs de janvier-mars tombaient 15–76 j après cette dose, quand la prédiction WHSAH est RÉSIDUELLE (184 → 1 pg/mL) → ratios aberrants (3,3 / 15,9 / 89,6 / 383) → la MÉDIANE tombait à ×2,21, **dans les deux pipelines** (manuel ET auto — même défaut de frontière). FIX : **garde de significativité** (`labIsSignificant`) — un lab ne calibre que si la prédiction reste ≥ 15 % du max déjà observé (l'injection « gouverne » encore le taux) ; appliquée aux 3 pipelines (manuel, échelles par ester, k T), labs traités par timestamp croissant. Résultat : le facteur WHSAH retombe à **0,55** (physiologique). - **Hint UX** (demande) : dans l'éditeur de traitement, quand l'auto- calibration est ON, la carte Calibration affiche que le facteur manuel est ignoré à l'affichage (l'auto recouvre les courbes). ### Ajouté - **4 nouveaux tests** (168 au total / 146 sans les données locales) : reproduction épinglée du ×2,21 (facteur manuel WHSAH 0,55, plus jamais 2,21), garde de significativité (séquence réelle + cas aux frontières), cohérence auto↔manuel. ## [1.4.8] — 2026-09-07 (versionCode 28) ## [1.4.8] — 2026-09-07 (versionCode 28) ### Corrigé - **La calibration automatique était fausse pour les modèles autres que le modèle stocké du traitement** (#60, remontée : « la calibration auto sur WHSAH met ×2,21 alors que l'idéal serait < 1 ») : un SEUL jeu de facteurs était calculé depuis la prédiction du modèle STOCKÉ puis partagé par toutes les courbes — en basculant un traitement (ex. EEn TFS → WHSAH), les facteurs devenaient absurdes (les prédictions WHSAH diffèrent de TFS : creux plus profonds, montée plus rapide). FIX (demande : « la calibration doit être active sur tous les modèles mais s'adapter à chaque profil ») : **la calibration est désormais PAR MODÈLE** — le graphique calcule un jeu de facteurs (et de k T) PAR modèle affiché (ESE/TFS/WHS), chacun calibré avec la prédiction DE SON modèle → chaque courbe calibrée passe par tes labs, quel que soit le modèle superposé. Home conserve la calibration du modèle stocké (inchangé). Épinglé par test : chaque courbe calibrée passe par le lab (±2 %) et les échelles TFS/WHS diffèrent. ### Ajouté - **5 nouveaux tests** (164 au total / 142 sans les données locales) : `CalibrationPerModelTest` — les 3 modèles calibrés atterrissent sur le lab (ESE inclus), échelles distinctes (ESE↔TFS ~2 %, WHS ~11 %), délégation `autoCalibrated(modelOverride)` → `computeEsterScaleFactors` (implémentation unique), **k T par modèle** (k ∝ 1/E2 calibrée — et les k post-calibration CONVERGENT entre modèles : même E2 cible, comportement voulu), labs antérieurs à la 1ʳᵉ dose → échelles vides (par modèle). ## [1.4.7] — 2026-09-07 (versionCode 27) ### Modifié - **Toggles de modèles pré-cochés selon TES traitements** (#58, demande : « les seules courbes activées par défaut sont celles qui se rapportent au modèle PK des traitements en cours ou passés ») : au chargement du graphique, les chips Estrannaise/Transfem/WHSAH sont alignés sur les modèles RÉELLEMENT utilisés par des traitements à PROFIL PK (`usesProfileModel` — le traitement inactif compte, l'oral/anti-androgène non) ; les autres restent activables au tap. Cas de l'utilisatrice : EV historique TFS + EEn actuel (qu'elle passe sous WHSAH) → TFS + WHSAH ON, Estrannaise OFF. Garde-fou testé : aucun modèle profil utilisé → TFS ON (les traitements Bateman sont tracés identiquement dans chaque série). Une seule initialisation : les toggles ensuite modifiés ne sont pas re-rencontrés. (`defaultModelToggles`, helper pur testé.) ### Corrigé - **La carte de traitement affichait « EEN Estrannaise » pour un traitement sous WHSAH** (#59, remontée) : le ternaire à 2 branches du chip `TreatmentsScreen` tombait WHSAH dans le `else` → label « Estrannaise ». FIX : helper centralisé **`modelLabelRes`** (unique source de vérité du label : ESE/TFS/WHS) utilisé par la carte — le dropdown de l'éditeur et le chip du graphique étaient déjà corrects. Règle : TOUT nouvel affichage de modèle passe par le helper, jamais un ternaire. ### Ajouté - **Épinglage : la calibration s'applique à TOUTES les courbes** (2ᵉ partie de la demande — vérification) : le facteur par ESTER (`scalePerEster`) est appliqué par DOSE (`doseEster`), indépendamment du modelOverride → les 3 courbes (ESE/TFS/WHS) d'un même traitement sont scalées IDENTIQUEMENT (2× le facteur = 2× la courbe, test `calibration applies to ALL model curves equally`). C'était déjà le comportement ; il est désormais garanti par test. Subtilité documentée (§7.6) : le facteur est calculé depuis la prédiction du modèle STOCKÉ du traitement — les courbes des autres modèles partagent ce facteur (un facteur par ester/période, pas par modèle). - **3 nouveaux tests** (158 au total / 136 sans les données locales) : `defaultModelToggles` (2) + calibration universelle (1). ## [1.4.6] — 2026-09-07 (versionCode 26) ### Ajouté - **3ᵉ modèle PK superposable : WHSAH** (demande : « ajouter le nouveau modèle à côté de tous les autres comme un 3ᵉ choix possible, au même niveau ») : le fit « license-free » du **WHSAH Collective** publié dans l'app open-source **Mona** (libre d'usage par leurs auteurs — crédit : alix / WHSAH Collective via Mona). Même famille mathématique (tri- exponentielle en forme close) mais paramètres refittés avec une **biodisponibilité explicite F < 1** : montée plus rapide à J+1 (EEn ~70 pg/mL vs ~22 chez TFS pour 5 mg) et t½ terminale plus longue (EEn 7,3 j vs 4,5 j). **PEP non couvert** par ce fit (comme chez Mona). - **6 presets WHSAH** (EV/EU/EEn/EB/EC/ECS) + dropdown « PK model » à 3 choix dans l'éditeur ; l'override d'ester par dose suit la couverture du modèle (helper `Esters.choicesForModel`, testé : TFS=7 / WHS=6 / ESE=3). - **Écran Graphiques : 3 toggles de modèles indépendants** (Estrannaise / Transfem Science / WHSAH superposables simultanément — légende en 3 couleurs : bleu / turquoise / vert) + **l'écran défile verticalement** (la rangée des toggles était coupée sous le pli, #57). - **7 nouveaux tests** (155 au total / 133 sans les données locales) : `WhsahModelsTest` (fidélité au comportement de Mona : EV 367,5 @ 1,69 j, EEn 187,9 @ 4,99 j, t½ 7,34 j), `WhsahEngineTest` (dispatch, coupure 10×t½, PEP → 0, 3 courbes distinctes), `choicesForModel`. ### Corrigé - **Écran Graphiques : rangée des toggles de modèles coupée sous le pli** (#57, découvert en validant sur émulateur) : le Column ne défilait pas — le chip WHSAH était littéralement hors écran sur un téléphone standard. FIX : écran à défilement vertical. ## [1.4.5] — 2026-09-07 (versionCode 25) ### Corrigé - **Boucle de recomposition sur le graphique** (#56, découvert en validant #54) : `nowMs` était relu frais à CHAQUE recomposition → `endMs` dérivait → les keys du producteur de courbes changeaient à chaque frame → re-calcul perpétuel → main thread saturé (« Skipped 52 frames ») → les taps sur les chips étaient perdus. FIX : `nowMs` mémoïsé sur le tick minute ; le calcul des créneaux prévisionnels mémoïsé aussi. - **La projection paraissait décalée d'un jour** (#55, remontée : « la prévision simule mon injection le 13 au lieu du 12 ») : diagnostic sur données réelles — la GÉNÉRATION est exacte (créneau = dernière dose loguée + intervalle exact ; samedi 12 à 18:54) ; l'illusion venait de (a) l'ABSENCE de marqueur des doses simulées, (b) les labels X alignés sur minuit **UTC** (= 02:00 du matin FR), (c) la courbe qui continue de descendre après le créneau (physiologie EEn — contribution à J+0,5 ~4 pg/mL). ⚠️ Subtilité documentée : la dose d'hier avait été LOGUÉE le lendemain de l'injection réelle → le rythme 6 j décale d'un jour par cycle ; l'app suit les LOGS (édition de la dose pour recadrer). ### Ajouté - **Marqueurs de doses sur le graphique** : les doses PRÉVISIONNELLES (ligne verticale pointillée + triangle + heure, dans le fuseau de lecture) et les doses RÉELLES (petits triangles discrets au bas) — la projection devient non ambiguë. - **Labels X alignés sur minuit LOCAL** (`xLabelTicks`, testé) au lieu de multiples UTC. - **Fuseau horaire du graphique configurable** : Paramètres → « Fuseau horaire du graphique » (vide = fuseau du téléphone) ; s'applique à l'alignement des jours et à l'heure des marqueurs. - **6 nouveaux tests** (145 au total / 123 sans les données locales) : ticks minuit local, fuseau choisi (America/New_York), cas dégénérés, créneau prévisionnel = dernière dose + intervalle exact (régression #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) ### Corrigé - **Le delta « vs il y a 6 h » de la carte d'accueil comparait en réalité il y a ~24 h** (#53, remontée : « 238 pg/mL avec −11 vs 6 h malgré mon injection d'hier ») : `curve.firstOrNull { écart ≥ 6 h }` sur une fenêtre de 24 h retournait le PREMIER point de la liste (il y a 24 h), pas un point à 6 h. FIX : nouveau helper pur **`pointHoursBefore`** (le point dont l'écart au dernier est le plus petit au-dessus de l'offset — testé : grille 1 h, grille 30 min, points irréguliers, courbe courte). Le delta affiché est désormais VRAIMENT un delta 6 h. ### Ajouté - **Régression n°4** épinglée sur le 4ᵉ export réel (HORS dépôt, gitignoré) : le scénario « injection EEn hier, l'estimation ne remonte pas » — export v1.4.2 (3 traitements, 52 doses, 26 labs, settings v2 avec seuils et auto-calibration). Le test épine, avec le VRAI moteur : la **contribution d'une dose EEn à J+1 est marginale** (< 12 % du total — Tmax 6,5 j), le **plateau d'équilibre** (swing 48 h ≤ 25 % du niveau), l'absence de fausse alerte à l'observation, et le parsing des settings v2. Réponse à la question posée : **comportement pharmacocinétique normal** de l'énanthate (courbe plate en équilibre), pas un bug — le seul bug était l'étiquette du delta (ci-dessus). - **8 nouveaux tests** (137 au total / 115 sans les données locales) : `RegressionUserCase4Test` (4) + `pointHoursBefore` (4, dans ChartZoomTest). ## [1.4.2] — 2026-09-07 (versionCode 22) ### Ajouté - **Seuils d'alerte configurables** (E2 et T) : dans Paramètres, une limite HAUTE et une limite BASSE par marqueur (E2 en pg/mL, T en ng/mL), chacune optionnelle (champ vide = pas d'alerte), validées (haut > bas requis) et **évaluées contre le taux ESTIMÉ actuel** (le même que la carte « niveau actuel », calibration incluse — pas une mesure de labo). En cas de dépassement : carte d'avertissement rouge sur l'accueil (« ▲ E2 estimé ≈ 259 pg/mL — au-dessus de ta limite haute (200) ») + disclaimer « estimation, pas une mesure ». Comparaison STRICTE (valeur == limite → rien). - **Notification d'alerte** : la vérification tourne **toutes les 15 min même app fermée** (worker WorkManager — la dépendance déclarée depuis v1.0 est enfin utilisée) + **check immédiat au save des seuils**. Anti-spam : l'état des alertes déjà notifiées est mémorisé — pas de re-notification du même franchissement ; le retour à la normale ré-arme la prochaine alerte ; pas de notification de « retour à la normale ». Canal de notification dédié (réglable séparément des rappels, remonte sur la Watch GT 3). Journalisation AppLog complète (debug à distance). - **9 nouveaux tests** (129 au total / 111 sans les données locales) : `AlertsTest` (12 : logique pure des seuils, codec d'état anti-spam) et `AlertsEngineTest` (4 : la notification évalue EXACTEMENT le niveau de la carte accueil ; déclenchement sur niveau réel). - **Backup JSON avec les paramètres utilisateur** : l'export embarque désormais langue, auto-calibration et seuils d'alerte (`"settings"`, format v2) ; l'import les restaure (la langue est appliquée au retour). **Rétrocompatible** : les anciens exports (sans settings) restent importables ; 2 tests round-trip + compat v1. ## [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) ### Ajouté - **Modèle Transfem Science reconstruit sur la méta-analyse officielle** (https://transfemscience.org/articles/injectable-e2-meta-analysis/) : la courbe TFS n'est plus dérivée des tables horaires du tableur — elle est désormais la **forme close exacte du modèle à 3 compartiments (V3C)** publié par le simulateur TFS, avec les paramètres D/k1/k2/k3 des 7 esters (EV, EEn, EU, EB, EC huile, EC suspension, PEP). PICS DE RÉFÉRENCE REPRODUITS À ~1 % (épinglés par `TransfemScienceModelsTest`) : EV 295 pg/mL @ 2,1 j ; EEn 160 @ 6,5 j ; EB 971 @ 0,65 j ; EC 155 @ 4,3 j ; EC susp. 241 @ 1,2 j ; PEP 34 @ 32,5 mg @ 18 j ; AUC et état d'équilibre EV 5 mg/7 j (Cmax 384 / Cmin 142 / Cavg 269) conformes à la Figure 11. Avantages : plus de plancher d'arrondi 0,01 en queue de table, pas de coupure à 8001 h (extrapolation exacte), coupure moteur à 10 demi-vies terminales. - **Nouveaux esters** : benzoate (EB), cypionate huile (EC), cypionate suspension aqueuse (ECS) et polyestradiol phosphate (PEP) — 4 presets TFS, override d'ester par dose étendu (la liste suit le modèle du traitement : TFS = les 7 esters, Estrannaise = EV/EU/EEn faute de tables ODS). PEP = biodisponibilité réduite, doses ~6,5× (cf article). - **Rappels qui suivent la Posologie** (fix #52, Corrigé ci-dessous) : la carte « Prochaine dose » de l'accueil passe par le même moteur que les alarmes. - **12 nouveaux tests** (102 au total / 84 sans les données locales) : `TransfemScienceModelsTest` (pics, t½, AUC, équilibre de la Figure 11) et `ReminderScheduleTest` (grille Posologie, créneau manqué, fallback quotidien). ### Corrigé - **Les rappels se déclenchaient TOUS LES JOURS** sans tenir compte de l'intervalle entre prises (#52, remontée utilisateur : « je m'injecte tous les samedis à 18 h, je dois recevoir le rappel uniquement le samedi à 18 h ») : le calcul du prochain déclenchement ignorait doses et Posologie. Désormais, un traitement avec Posologie + historique sonne le jour du créneau à l'heure choisie (dernière dose + k × intervalle, créneaux passés sautés comme la prévision) ; sans Posologie ou sans historique → comportement quotidien inchangé. De plus, l'alarme suivante est reprogrammée **après chaque notification** et **après « Pris »** (les alarmes sont one-shot : la chaîne n'existait qu'au boot/au save). ## [1.3.5] — 2026-09-06 (versionCode 19) ### Corrigé - **Création du calendrier d'agenda impossible** (remontée via les logs de diagnostic — le mécanisme v1.3.1 a tout de suite nourri le diagnostic à distance : `Sync adapters must specify an account and account type`) : l'URI d'insertion `CALLER_IS_SYNCADAPTER=true` du calendrier local « HormoneTrack » n'embarquait pas `ACCOUNT_NAME` + `ACCOUNT_TYPE` — exigé par le CalendarProvider pour toute opération sync-adapter (`CalendarEvents.ensureCalendar`, §14 #50). Ces mêmes logs prouvent aussi que **la permission agenda est désormais accordée** (fix manifest v1.3.3 ✓) et que **l'export des logs fonctionne sur le téléphone** (fix #48 ✓) — la chaîne de diagnostic est opérationnelle. - **Suppression d'événement d'agenda silencieusement manquée** (#50 bis) : l'URI de DELETE avait le même défaut (`CALLER_IS_SYNCADAPTER` sans params account) → « switch OFF » + save loguait « supprimé » mais l'événement restait dans le provider. Fix : mêmes paramètres sur l'URI de delete. Validé émulateur : cycle complet ON→save→OFF→save = événement créé puis réellement supprimé (`content query` vide). - **Événement d'agenda orphelin après tout re-save** (#51) : `TreatmentEditorScreen.buildTreatment()` ne transmettait pas `calendarEventId` → l'id revenait à `null` à chaque sauvegarde : après le premier save, l'événement devenait définitivement orphelin (plus jamais supprimable/désactivable par l'app). L'id existant est désormais chargé à l'ouverture et conservé par le save. - **Log « événement agenda supprimé » mensonger** : imprimé aussi quand rien n'a été supprimé (switch désactivé, posologie absente, permission absente). Désormais trois messages distincts : « créé/mis à jour (id=…) », « supprimé », « non activé (switch/posologie/permission) ». ### Ajouté - Validation émulateur de l'agenda end-to-end (recette §16.ter, permissions accordées via `pm grant`) : calendrier + événement récurrent créés et visibles dans le provider (`content query`). ## [1.3.4] — 2026-09-06 (versionCode 18) ### Corrigé - **L'écran Doses faisait crasher l'app** (remontée) : `MissingFormatArgumentException: Format specifier '%3$d'` — la string `hrt_duration` a TROIS placeholders (`%1$d mois, %2$d jours, %3$d total`) mais l'appel transmettait 2 arguments. Crash dès le premier affichage de l'écran Doses avec une donnée antérieure à aujourd'hui. **Le bug est né en v1.3.1** (string + appel dans le même commit, jamais testés ensemble) et a survécu aux releases v1.3.1→v1.3.3. Fix : passer totalDays en 3ᵉ argument (cf §14 #47). - **L'export des logs de diagnostic faisait crasher l'app** (remontée répétée ; les « fixes » IO de v1.3.2 et v1.3.3 étaient corrects mais à côté du vrai problème) : le nom de fichier était construit via `LocalDate.now().format(ofPattern("yyyyMMdd-HHmm"))` — un `LocalDate` n'a pas de champ horaire → `UnsupportedTemporalTypeException: Unsupported field: HourOfDay`, levée SYNCHRONEMENT dans le onClick (thread UI) au tap sur « Exporter », avant même l'ouverture du gestionnaire de fichiers. L'export JSON fonctionnait car son pattern `yyyyMMdd` est valide pour un LocalDate. FIX : utilitaire `ExportFileNames` (pur, testé) — `diagnosticLogFileName()` utilise un `LocalDateTime` ; `backupFileName()` reste sur `LocalDate`. Cf §14 #48. - **Lint `StringFormatMatches`** (détecté en implantant le lint — nouveau filet de la v1.3.4) : la notification de rappel passait un Double à une `%s`. Fix : `dose.toString()` (comportement d'affichage inchangé, #49). ### Ajouté - **Lint en filet bloquant** : `./gradlew lint` est désormais vert (it aurait attrapé dès v1.3.1 les deux crashs ci-dessus via `StringFormatMatches`). Les deux checks Compose 1.12+ théoriques (`NonObservableLocale`, `LocalContextGetResourceValueCall`) sont rétrogradés en warning via `app/lint.xml` (staleness de config, refonte i18n plus tard, cf §19/§20). - **Recette reproductible : test manuel sur émulateur** (documentée §16.ter) : émulateur + image ATD ou google_apis, seed de la DB du debug avec un backup JSON via le nouveau script **`scripts/seed-emulator.py`** (générique, sans aucune donnée embarquée), installation du APK release par-dessus (données conservées, même signature), pilotage par `uiautomator dump` + `input tap`, crashs via `adb logcat -b crash`. C'est cette recette qui a reproduit et confirmé les deux crashs ci-dessus ainsi que LEUR correction (flux SAF complet : picker → SAVE → fichier écrit → message de confirmation, sur l'APK release données réelles). - **3 nouveaux tests** (`ExportFileNamesTest`, 90 au total / 72 sans les données locales) : épinglent le format des noms de fichiers d'export — auraient attrapé le bug #48 le jour même. ## [1.3.3] — 2026-09-06 (versionCode 17) ### Corrigé - **Export des logs plantait TOUJOURS l'app** (remontée ; le « fix » v1.3.2 était insuffisant) : l'écriture était réimplémentée en inline au lieu de réutiliser l'IO éprouvée de l'export JSON, et les appels `AppLog.log` du callback étaient hors try/catch — une exception dans une coroutine à scope racine (CoroutineScope standalone) remonte au handler de la thread = crash du process. FIX : l'export passe désormais par **`BackupManager.writeBackup` tel quel** (le même code path que l'export JSON qui fonctionne sur le téléphone), tout le callback est gardé, et `AppLog.log` ne lève plus jamais d'exception (IO fichier avalée en interne, on perd au pire une ligne de journal). Feedback visible succès/échec (comme l'export JSON) + compteur de lignes rafraîchi après l'export. - **Événements d'agenda inopérants depuis v1.3.0** : les permissions WRITE_CALENDAR / READ_CALENDAR n'étaient **pas déclarées dans le manifest** (jamais présentes dans tout l'historique git, bien que documentées §9) — la demande à l'exécution était donc refusée d'office et `CalendarEvents` lève SecurityException. Cause racine manquante des bugs « toggle agenda » « corrigés » en v1.3.1/v1.3.2 : la logique UI était correcte, il ne venait à l'esprit de personne que la déclaration manquait. - **Métadonnées de version fausses** : le bump de version n'avait jamais été commité — les tags v1.3.0, v1.3.1 **et** v1.3.2 contenaient tous `versionCode = 14 / versionName = "1.3.0"`, donc les APK publiés affichaient « 1.3.0 » dans Paramètres et en titre du dialog « Nouveautés ». Désormais bumpé + commité avant chaque tag (checklist §16 étape 1). ### Technique - Doc de développement mise à jour (DB v3, targetSdk 36, fichiers de données de régression, bugs #43–#45) ; téléphone de test documenté : **Google Pixel 9 /e/OS** (AOSP, DocumentsUI standard — l'export JSON y fonctionne, l'export logs réutilise ce même path). - Compteur de tests inchangé : 87 (44 sans les données locales, gitignorées). ## [1.3.2] — 2026-09-06 (versionCode 16) ### Corrigé - **Toggle de l'agenda** : le switch ne s'activait pas (le résultat de la permission arrive ASYNCHRONE — le test synchrone juste après `launch()` était toujours faux). FIX : le callback active le switch ; Posologie désormais **requise** pour l'agenda (refus + message si absente) ; permission vérifiée RÉELLEMENT via `ContextCompat` au save (le flag remembered pouvait être faux après recomposition) ; journalisation AppLog à chaque étape. - **Export des logs** plantait l'app : réécrit avec le pattern éprouvé de l'export JSON (BackupManager) ; chaque étape journalisée dans AppLog. - **Dialog « Nouveautés » réapparaissait à chaque réouverture** : la version vue est désormais mémorisée AVANT l'affichage (et non après) + AppLog pour diagnostiquer si ça se reproduit. - **Titre du dialog « Nouveautés »** : affiche la version de l'APP (`BuildConfig.VERSION_NAME`) — si l'APK installé est 1.3.0, le titre montre 1.3.0 même si le contenu parle d'une version plus récente (l'asset changelog.md peut contenir des sections plus récentes si l'APK a été reconstruit après coup). Installer le dernier APK (`HormoneTrack-v1.3.2-release.apk`) pour avoir la version correcte. ## [1.3.1] — 2026-09-06 (versionCode 15) ### Ajouté - **En-tête « temps sous THS »** sur la page Doses : depuis la 1re prise enregistrée, en jours totaux et décomposition mois/jours (mois = 30 j, estimation documentée). - **Logs de diagnostic** (v1.3.1) : journal horodaté des événements notables (agenda, rappels, import/export, erreurs), buffer circulaire de 500 lignes, persisté dans `filesDir/debug-log.txt` ; export (SAF) et effacement depuis Paramètres. Objectif : debug à distance via les logs joints aux retours. - 9 nouveaux tests (87 au total) : AppLog (formatage, buffer circulaire) et HrtDuration (jours, décomposition mois/jours). ### Corrigé - **Toggle de l'agenda ne s'activait pas** (bug remonté) : le résultat de la permission arrive ASYNCHRONE — le test synchrone juste après `launch()` était toujours faux → le switch rebondissait sans s'activer et sans feedback. FIX : le callback active le switch ; feedback visible (hint) et journalisation (AppLog). ## [1.3.0] — 2026-09-06 (versionCode 14) ### Ajouté - **Dialog « Nouveautés »** : après chaque mise à jour (et à la première installation), l'app affiche le changelog des versions non vues dans une fenêtre fermable — ne réapparaît pas avant la prochaine mise à jour (version vue mémorisée). Contenu = `docs/CHANGELOG.md` embarqué en asset, synchronisé automatiquement à chaque build (tâche Gradle `copyChangelog`). - **Événements d'agenda récurrents** : dans l'éditeur de traitement, sous « Rappels », un switch « Événement d'agenda récurrent » crée un événement dans un calendrier local dédié « HormoneTrack » (visible dans l'agenda du téléphone) avec répétition RRULE dérivée de la **Posologie** (tous les N jours, arrondi demi-supérieur). Permission agenda demandée à l'activation ; suppression/recréation automatique au save. - **Paramètres : version installée + lien cliquable** vers les releases Gitea (télécharger les APK sans attendre le rappel de mise à jour). - 11 nouveaux tests (78 au total) : ChangelogHelper (8 : comparaison SemVer numérique 1.2.9 < 1.2.10 — lexicographique aurait tort, extraction de sections, première installation), CalendarRruleTest (3 : RRULE, arrondi demi-supérieur — **kotlin.math.round arrondit les ties vers l'entier PAIR (6,5 → 6)**, piège documenté). ## [1.2.10] — 2026-09-05 (versionCode 13) ### Corrigé - **Sens des boutons de zoom inversé** (retour utilisateur) : convention carte — « **+** » = zoom **avant** (fenêtre plus courte, plus de détail) et « **−** » = zoom **arrière** (fenêtre plus longue, plus de temps). Avant, le « + » dézoomait (montrait plus de temps) — pas intuitif. Le pinch (écarter = zoom avant) était déjà correct. ## [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) ### Modifié - **« Fréquence d'injection » renommé « Posologie »** (section Traitements) : le terme « fréquence d'injection » n'avait pas de sens pour les prises d'hormones non injectables (gel, patch, oral). L'astuce d'intervalle est inchangée ; les textes d'aide disent désormais « dernière **prise** enregistrée » au lieu de « dernière injection » (EN : « Dosage »). ## [1.2.7] — 2026-09-05 (versionCode 10) ### Corrigé - **Prévision cassée après un oubli d'injection** : le premier créneau projeté tombait dans le PASSÉ (dernière prise + intervalle = jour de l'injection manquée) → la courbe peignait un **faux pic dans l'historique** et tout le rythme projeté était décalé. Désormais les créneaux déjà passés ne sont jamais simulés : la prévision avance au premier créneau **strictement futur** au rythme configuré. Un simple **retard**, lui, décale naturellement toute la prévision (elle part de la dernière prise réelle — comportement voulu, désormais testé). - 2 nouveaux tests (64 au total) : oubli (créneaux passés sautés, espacement conservé) et retard (décalage complet de la prévision). ## [1.2.6] — 2026-09-05 (versionCode 9) ### Ajouté - **Valeurs estimées sur les pics/creux** : chaque triangle ▲▼ du graphique est accompagné de la valeur simulée de l'extremum (au-dessus d'un pic, en dessous d'un creux), dans la couleur de la courbe. ### Corrigé - **Import JSON en mode ÉCRASEMENT** : l'import échouait dès que l'app contenait déjà des données (conflit d'IDs du backup avec les IDs existants). Désormais l'import efface d'abord les données actuelles (ordre enfants → parents), restaure le backup (IDs conservés → FK valides), restaure le réglage T du backup et **reprogramme les rappels**. Le dialog d'import prévient clairement : « Effacer & restaurer ». ## [1.2.5] — 2026-09-05 (versionCode 8) ### Corrigé - **Espace vide en haut de l'écran** (contenu trop bas) : double consommation des insets — la fenêtre poussait déjà le contenu sous la barre de statut (thème AppCompat) ET les `TopAppBar` Material 3 rajoutaient leur padding de barre de statut interne. Passage en **edge-to-edge propre** (`enableEdgeToEdge()`) : - les `TopAppBar` consomment la barre de statut (une seule fois) ; - les `Scaffold` (racine + imbriqués Doses/Analyses) ont `contentWindowInsets = 0` pour ne pas cumuler ; - la `NavigationBar` consomme la barre de navigation système. Le contenu remonte, les listes défilent visuellement sous la barre de statut (comportement Material You standard). - **Builds release optimisés** (R8 + shrink des ressources, signés avec la clé debug → installables par-dessus sans perte de données) : l'APK passe de **20 Mo à 2,4 Mo** et gagne en fluidité. Depuis cette release, chaque version est publiée en deux fichiers : - `HormoneTrack-vX.Y.Z-release.apk` ← **recommandé** (optimisé R8) ; - `HormoneTrack-vX.Y.Z-debug.apk` (comme avant, plus lourd, pour diagnostic). ## [1.2.4] — 2026-09-05 (versionCode 7) ### Corrigé - **Traitement inactif = drapeau administratif, pas filtre de données** (bug remonté) : passer un traitement à inactif faisait disparaître sa simulation du graphique **et** sa contribution à la calibration, tout en permettant encore d'y loger des doses (incohérence). Désormais : - Home et Graphiques simulent avec **TOUS les traitements** (actifs + inactifs) → l'historique EV d'un traitement passé à inactif reste simulé et calibré ; - l'inactivation retire uniquement : les chips « Log rapide », le traitement dans le dropdown des **nouvelles** doses (l'édition d'une dose existante garde son traitement même inactif), et les **rappels** (alarme annulée au save, au boot et dans le calcul de la prochaine dose) ; - la suppression d'une dose reste possible depuis l'historique. ### Ajouté - **Régression n°3** épinglée sur le 3ᵉ export réel (HORS dépôt, gitignoré) : le scénario complet de transition — 1 traitement **EV inactif** (29 doses, 2–8 mg, janvier→juillet) + 1 traitement **EEn actif** (9 doses), 22 labs couvrant les deux périodes. Vérifie : simulation de l'inactif, calibration par période sur les DEUX esters (EV **et** EEN), k T par ester, continuité de la courbe pendant la transition, niveau actuel. 6 nouveaux tests (62 au total, tous verts). ## [1.2.3] — 2026-09-05 (versionCode 6) ### Ajouté - **Pics et creux estimés sur le graphique** : chip « Pics / creux » (désactivé par défaut) qui affiche des triangles ▲▼ aux extrema locaux de CHAQUE courbe (E2 et T, pour les deux modèles), colorés comme leur série. Détection dans le moteur (`detectExtrema`) : extrema locaux stricts, plateaux fusionnés, alternance pic/creux imposée, oscillations d'amplitude < seuil ignorées (2 pg/mL pour l'E2, 0,02 ng/mL pour la T — sémantique zigzag : une oscillation sous le seuil produit un seul pivot). - **Calibration T PAR PÉRIODE D'ESTER** : la suppression de la testostérone n'est pas la même selon l'ester (valerate = pics hauts et courts, enanthate = plateau doux) → le k du modèle T est désormais calibré **par période** comme le facteur d'échelle E2 (`computeTKPerEster`) : labs T valerate → k des doses valerate, labs T enanthate → k des doses enanthate. La courbe T utilise à chaque instant le k de l'ester **actif** (dernière dose ≤ t, `activeEsterAt`), fallback = k stocké. Le k T est calibré contre l'E2 **déjà calibrée** (scalePerEster appliqué). - **Données de test hors dépôt (vie privée)** : les tests de régression épinglés sur les exports réels chargent désormais leurs données depuis `local-test-data/` (**gitignoré**) et s'ignorent proprement si le fichier est absent — **aucune donnée de santé personnelle dans le dépôt ni dans son historique** (réécriture d'historique effectuée avant le premier push). - 14 nouveaux tests (56 au total) : détection d'extrema (6) + attribution du k T par période et courbe T utilisant le k de l'ester actif (2), regroupement des prises de sang (4), régression n°2 en version data-driven (6). ### Corrigé - La calibration automatique de la T utilisait l'E2 **non calibrée** → les k étaient faussés ; elle utilise maintenant l'E2 calibrée par période d'ester. ## [1.2.2] — 2026-09-05 (versionCode 5) ### Ajouté - **Prise de sang E2 + T en UNE entrée** : le dialog d'ajout propose les deux marqueurs (chacun **optionnel** — on peut ne saisir que E2 ou que T), avec date/heure et notes partagées ; les entrées sont insérées au même timestamp et s'affichent **côte à côte** dans l'écran Analyses (« E2 306 pg/mL · T 44 ng/dL »). - **Tap sur une paire → sélecteur** (« Quelle entrée veux-tu modifier ? » E2 / T) puis édition pré-remplie de l'entrée choisie. - **Suppression par prise de sang** : la corbeille supprime la prise entière (confirmation listant les valeurs). - **Toggle T du graphique** : masque désormais aussi les **labs T** (avant : seule la courbe T disparaissait, les points restaient). - 4 tests de regroupement (48 au total). ## [1.2.1] — 2026-09-05 (versionCode 4) ### Corrigé - **Calibration par PÉRIODE D'ESTER** (le fond du problème « courbes à 250–375 ») : la calibration automatique calculait UN facteur par traitement, mélangeant les labs de toutes les périodes — un lab valerate comparé à une prédiction enanthate donne un ratio aberrant qui gonfle la courbe. Désormais chaque lab est **attribué à la période d'injection dans laquelle il tombe** (dernière dose E2 ≤ lab → son ester) et le facteur est la médiane des ratios de CETTE période : labs valerate → doses valerate, labs enanthate → doses enanthate (`computeEsterScaleFactors` + paramètre `scalePerEster` dans `e2At`/`computeCurve`). Vérifié sur les données réelles : l'état d'équilibre EEn (t½ ≈ 6,7 j, doses tous les 6–7 j → accumulation ×2) donne ~270 pg/mL calibré — cohérent avec les labs 300/250 ; les « 375 » rapportés = le même calcul **non calibré** (268/0,72 ≈ 367). - **Lab T saisi en "pg/mL"** (38 "pg/mL" — faute de frappe) : renvoyé brut, il écrasait l'axe T (~40) et **rendait la courbe T invisible** (écrasée en bas). Conversion défensive ajoutée (`pg` → ÷1000, `µg/L` → ÷1000). ### Ajouté - **Édition des résultats de labo** (comme les doses) : appuyer sur une ligne de l'écran Analyses ouvre le dialog pré-rempli (marqueur, valeur, unité, date/heure, notes). Nouveau formateur `formatLabValue` (préserve les décimales : 0,44 reste 0,44, contrairement à formatDose). - **Tap sur le graphique de l'accueil → écran Graphiques** + mini-légende (E2 bleu / T rose) + indication. - **Calibration automatique fusionnée** : UNE seule option dans Paramètres qui couvre **E2 (par période d'ester) ET T** — plus de réglage T isolé ; le bouton manuel « Calibrer avec les analyses » (T) reste comme action ponctuelle. - **8 nouveaux tests** (44 au total) dont `RegressionUserCase2Test` épinglé sur le 2ᵉ export réel (9 doses EEn/TFS, 8 labs, fréquence 6 j) — inclut la vérification de l'état d'équilibre et du lab T en unité aberrante. ### Modifié - `AutoCalibrated` : `treatments` inchangés (plus de copies avec SF modifié), nouveau champ `esterScales: Map` + `calibratedEsters`. ## [1.2.0] — 2026-09-05 (versionCode 3) ### Ajouté - **Panoramique du graphique** : fais glisser le graphique vers la droite pour remonter dans le passé (fenêtre glissante sur tout l'historique) ; bouton « Revenir à maintenant ». - **Les deux modèles PK superposables** : Estrannaise (bleu) et Transfem Science (turquoise) tracées simultanément sur le même graphique, avec toggles indépendants (override de modèle dans le moteur, indépendant du modèle stocké par traitement). - **Simulation prévisionnelle** : nouvelle section « Fréquence d'injection » dans l'éditeur de traitement (intervalle en jours) → chip « Prévision » du graphique qui projette les doses à venir (2× l'intervalle, borné 7–30 j) à partir de la dernière injection réelle ; les doses projetées ne sont jamais sauvegardées et conservent l'ester override de la dernière injection. - **Calibration automatique (optionnelle)** : dans les Paramètres, option « Calibration automatique » — ajuste à la volée (affichage seul) les facteurs d'échelle et le modèle T depuis les labs ; désactivée par défaut, les valeurs stockées ne sont jamais modifiées. - **Intervalle entre dosages** : l'écran Doses affiche Δ (jours) entre chaque dose et la précédente du même traitement (colonne « Interval (d) » du `.ods`). - 6 nouveaux tests (`V120FeaturesTest`) → **36 tests**. ### Modifié - **Montée des toolchains** : Gradle 9.7.1, **AGP 9.4.0** (Kotlin intégré — plus de plugin `org.jetbrains.kotlin.android`, `kotlinOptions` supprimé), Kotlin 2.3.21, KSP 2.3.11, **Compose BOM 2026.08.00** (Compose 1.12), Room 2.8.4, Navigation 2.10.0, Activity 1.13.0, AppCompat 1.8.0, DataStore 1.2.1, Gson 2.14.0, compileSdk/targetSdk **37**. - Note Material You / Expressive : la BOM récente apporte composants et comportements Material You ; `MaterialExpressiveTheme` reste **internal** dans la ligne material3 pinnée (vérifié par compilation) → `MaterialTheme` standard conservé, à basculer dès que l'API devient publique (commenté dans `ui/theme/Theme.kt`). ### Corrigé - **Horloge du TimePicker décentrée** (partait vers la gauche dans les dialogs de saisie) : centrage explicite (Box + Center) dans `DateTimeField` et l'éditeur de traitement. - Migration Room **v1 → v2** propre (colonne `forecastIntervalDays`) avec suppression du `fallbackToDestructiveMigration()` — les données réelles sont protégées : une migration manquante plante bruyamment au lieu d'effacer. ## [1.1.0] — 2026-09-05 (versionCode 2) ### Corrigé - **Courbes EEn plates à zéro** (le bug rapporté : « les graphiques ne se génèrent pas »). Cause : casse des clés de profils — l'asset JSON (verbatim du `.ods`) contient `"EEn_ese"`/`"EEn_tfs"` (n minuscule) alors que la constante `Esters.EEN = "EEN"` ; le lookup exact échouait silencieusement → `sample() = 0` pour **tous** les traitements EEn (EV et EU marchaient, ce qui masquait le bug). Fix : lookup **insensible à la casse** dans `PKProfileStore` (`lookup()`), test de régression épinglé sur les données réelles de l'utilisatrice (`RegressionUserCaseTest`, export v1.0.0 fourni). - **Unités de testostérone** : les labs T pouvaient être saisis en ng/dL (cas réel : 33 et 44 ng/dL). L'axe T du chart mélangeait alors des ng/dL avec des estimations en ng/mL (erreur ×100, courbe T invisible) et la calibration T aurait été faussée d'un facteur 100. Fix : `PharmacokineticEngine.convertTToNgMl()` (ng/dL → ×0,01 ; ng/L → ×0,001 ; nmol/L → ×0,2884 ; ng/mL inchangé), appliqué à la calibration **et au rendu du chart**. ### Ajouté - **Édition des doses** (le bug rapporté : « on ne peut pas modifier les entrées d'injection ») : appuyer sur une ligne de l'écran Doses ouvre le dialog en mode édition (traitement, dose, date/heure, notes) → enregistre sans recréer. - **Choix de l'ester par injection** dans le dialog de dose (comme dans le `.ods`) : « Défaut (EEn) », EV, EU, EEn — override stocké par dose, utilisé par le moteur PK. - 6 tests de régression sur les données réelles de l'utilisatrice (30 tests au total). ### Technique - `versionCode 2`, `versionName 1.1.0` ; APK debug régénérée. ## [1.0.0] — 2026-09-05 (versionCode 1) Première version fonctionnelle : - Courbes estimées E2/T heure par heure (24 h / 7 j / 30 j), modèles **Estrannaise** et **Transfem Science** (tables horaires extraites de `Estrogen.ods` : EV/EU/EEn) - Modèle Bateman paramétrable (gel/patch/oral) - Log des doses, analyses de sang, calibration (facteur d'échelle médian lab÷prédiction) - Estimation T empirique calibrable - Rappels quotidiens (alarmes exactes) avec actions « Pris » / « Reporter 1 h », notifications remontant sur Huawei Watch GT 3 (Gadgetbridge ou Huawei Health) - Sauvegarde/Restauration JSON (SAF), FR/EN (langue par app), 100 % local - 24 tests unitaires (moteur PK, profils ODS, round-trip backup)