HormoneTrack/docs/CHANGELOG.md
Siphonight b3372a80e4 v1.12.0 : fix #70 (inactif à Posologie projetait encore) + Doses (icônes de forme, marqueurs prise de sang)
- FIX #70 : generateForecastDoses garde !isActive — un injectable archivé
  avec Posologie et de l'historique projetait encore ses injections
  (graphique Prévision, extension de fenêtre, horizon LabTiming) ; le chip
  Prévision s'activait sur des Posologies inactives. FIX : garde moteur +
  horizon/chip sur les ACTIFS. §6.bis inchangé : l'historique reste simulé,
  seul le FUTUR s'arrête (comme les rappels v1.4.0). Miroir web. 3 tests
  ForecastDosesGuardTest + 3 miroirs web.
- Icônes de forme de prise dans Doses : routeIcon pur (pilule/seringue
  IM+SC teinte tertiary pour la SC/goutte/sparadrap/compte-gouttes ;
  ⚠️ MedicationLiquid absent d'icons-extended 1.7.8 → Colorize).
- Marqueurs « prise de sang le même jour » : labMarkersForDose pur (jour
  calendaire LOCAL, lab à l'heure exacte = APRÈS) → encart 🧪↑/🧪↓ en bout
  de ligne. 7 tests DosesExtrasTest + 10 miroirs web.

274 tests JVM + 14 UI + lint verts ; web 194 tests + E2E verts (check.sh).
Validé émulateur sur APK RELEASE avec données réelles seedées + scénario du
bug (2 inactifs à Posologie 7 j) : Prévision on/off 0 crash, glyphes et
marqueurs en place.
2026-09-29 23:24:34 +02:00

1400 lines
82 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.10`, …).
## [1.12.0] — 2026-09-29 (versionCode 49)
### Corrigé — un traitement inactif à Posologie projetait encore ses injections (#70)
- **Remontée** : « une injection qui n'est plus active est toujours prise en
compte dans le graphique de simulations si elle contient une posologie et
qu'elle a déjà été utilisée — un traitement mis en inactif ne devrait
plus être pris en compte pour les futures prédictions ».
- **Cause** : `generateForecastDoses` (source unique des créneaux
prévisionnels) ne vérifiait pas l'archivage ; de plus l'horizon du chip
Prévision du graphique incluait les Posologies INACTIVES.
- **Fix** : garde `!isActive` dans le moteur (l'historique reste simulé —
§6.bis inchangé — seul le FUTUR s'arrête, comme les rappels depuis
v1.4.0) + horizon/chip Prévision calculés sur les ACTIFS uniquement.
**Miroir web appliqué** (`js/pk/pk-engine.js` + `js/ui/chart.js`).
- Tests : 3 tests JVM (`ForecastDosesGuardTest`) + 3 miroirs web ;
émulateur avec données modifiées (2 inactifs à Posologie 7 j) :
Prévision on/off sans crash.
### Ajouté — icônes de forme de prise dans la page Doses
- Chaque ligne de dose porte désormais l'icône de sa forme : 💊-icône pilule
(oral), seringue (injection IM et SC — teinte distincte pour la SC),
goutte (gel), sparadrap (patch), compte-gouttes (autre). Miroir web :
émojis (`routeGlyph`, même mapping).
### Ajouté — marqueurs « prise de sang le même jour » sur les lignes de doses
- Une analyse de sang effectuée LE MÊME JOUR qu'une dose affiche un petit
encart latéral 🧪↑ (avant la dose) et/ou 🧪↓ (après) — pour visualiser
les prises de sang dans le tableau de dosages (demande utilisatrice).
Fenêtre = jour calendaire LOCAL (au-delà le lien clinique devient ténu).
Helper pur `labMarkersForDose` (Kotlin + miroir web) — 7 tests JVM
(`DosesExtrasTest` : mapping icônes + marqueurs, frontière minuit,
convention « à l'heure de la dose = après ») + 10 miroirs web.
- Vérifié émulateur avec données réelles : glyphes en tête de liste,
marqueur « Blood test later the same day » sur la dose du 10 sept
(analyse 35 min après).
Vérifié : **274 tests JVM** (240 sans données locales) + **14 tests UI** +
lint verts ; **web 194 tests + E2E verts** (`check.sh`).
## [1.11.0] — 2026-09-29 (versionCode 48)
### Ajouté — les traitements inactifs regroupés en bas de la page Traitements
- **Demande** : « les traitements inactifs sont peu discernables par rapport
aux traitements actifs — les placer à la fin (tout en bas) de la liste
pour éviter les confusions ».
- **Tri** : les ACTIFS d'abord, les INACTIFS regroupés **tout en bas**, sous
un en-tête de section « Traitements inactifs » (n'apparaît que s'il
existe des inactifs). Tri STABLE : l'ordre relatif à l'intérieur de
chaque groupe est conservé — le tri ne réordonne jamais les actifs entre
eux, ni les inactifs entre eux.
- **Distinction visuelle renforcée** : les cartes inactives sont ATTÉNUÉES
(alpha 0,55) en plus du badge « inactif » — second signal demandé
(« peut-être les rendre plus distincts ») ; elles restent lisibles et
tappables (l'édition d'un traitement archivé doit rester possible).
- Helper pur `treatmentsForDisplay` (Kotlin + **miroir web** en
`js/data/models.js`) — 5 tests JVM (`TreatmentsDisplayTest`) + 4 tests
web (`tests/treatments-display.test.js`).
- Vérifié émulateur avec données réelles seedées : les 5 actifs en haut,
en-tête puis les 2 inactifs (EV-old, EEn-old) tout en bas.
Vérifié : **264 tests JVM** (230 sans données locales) + **14 tests UI** +
lint verts ; **web 184 tests + E2E verts** (`check.sh`).
## [1.10.1] — 2026-09-28 (versionCode 47)
### Amélioré — durcissement du système de rappels (revue de code post-v1.10.0)
- **`launchMode="singleTop"` sur MainActivity** : un tap sur la notification
quand l'app est DÉJÀ ouverte délivre désormais `onNewIntent` (dialog de
saisie ouvert, extras traités) **sans détruire/recréer l'activité** —
l'ancien combo `standard` + `CLEAR_TOP` recréait tout et rendait le chemin
`onNewIntent` de la v1.10.0 inopérant (code mort).
- **Instant PLANIFIÉ dans l'alarme (`EXTRA_PLANNED_AT`)** : le garde
« créneau déjà honoré » (v1.10.0) évaluait le jour au moment du FEU — une
alarme inexacte (`setWindow`) ou retardée (Doze) glissant après minuit
évaluait le mauvais jour (23:58 → 00:02). L'instant planifié voyage
désormais dans l'intent de l'alarme, le snooze le FORWARDE tel quel, et
le garde évalue toujours le jour du CRÉNEAU. Vérifié sur émulateur par
broadcast avec `planned_at` daté d'hier → notification correctement
sautée.
- **`BootReceiver` consolidé** (dette §20.bis #2) : `goAsync` + coroutine IO
(pattern ReminderReceiver) — remplace le `Thread` + double `runBlocking`
de la v1.0 ; une seule lecture séquentielle des DAO, `finish()` garanti.
- Audit doc : comptes de tests périmés corrigés (§14 note #64, §16.ter).
Vérifié : 259 tests JVM + 14 tests UI + lint verts ; scénarios émulateur
(singleTop + planned_at). Web non concerné (aucune logique miroir touchée).
## [1.10.0] — 2026-09-27 (versionCode 46)
### Corrigé — la recommandation de prise de sang précédait la stabilisation (#68)
- **Remontée** : « la recommandation suggère une date antérieure à la
stabilisation des taux » (données réelles de la remontée → 6ᵉ régression
épinglée `local-test-data/backup-v1.9.8.json`, gitigné).
- **Cause (double)** : le « creux » était le **minimum de la fenêtre entière**
(créneau N−1 → créneau N). Pour un ester à montée lente (EEn : pic ~J+5 ≈
intervalle 7 j), ce minimum tombe **juste après l'injection PRÉCÉDENTE**
(creux d'absorption, ~3 h) — pas juste avant la suivante : la date proposée
était gouvernée par l'injection N−1, elle-même antérieure à la
stabilisation, et le texte « juste avant ton injection du … » était faux de
plusieurs jours. Balayage sur ±60 jours de `now` (test) : creux 5 oct 02:32
vs stabilisation 7 oct 04:03.
- **Fix** : le creux = **niveau PRÉ-INJECTION du créneau** (dernier point de
courbe avant l'injection) — restaure le principe v1.8.0 documenté (« juste
avant l'injection suivante, le moment le plus comparable »), rend la carte
cohérente (creux 46 min avant le créneau étiqueté) et le garde de
stabilisation s'exprime sur l'instant réellement proposé. Pour les esters à
t½ courte (EV), le point pré-injection EST le minimum de fenêtre :
comportement inchangé. **Miroir web appliqué** (`js/pk/lab-timing.js`).
- Tests : régression n°6 data-driven (4 tests dont un balayage de `now` sur
±60 j — l'invariant « creux ≥ stabilisation » doit tenir À TOUT INSTANT) ;
les 17 LabTimingTest passent inchangés (leur sémantique était celle du fix).
### Corrigé — le dialog de dose se rouvrait à chaque retour sur l'accueil (#69)
- **Remontée** : tap sur la notification de rappel → le dialog de saisie
s'ouvre ; le fermer puis changer d'onglet et revenir sur l'accueil le
ROUVRAIT à chaque fois (reproduit et revérifié sur émulateur).
- **Cause** : la demande vivait au niveau ACTIVITÉ (paramètre d'intent) mais
la consommation vivait dans un `LaunchedEffect` de Home — quitter l'onglet
détruit les `remember` locaux, le paramètre survivait → à chaque
ré-entrée en composition, le dialog se rouvrait.
- **Fix** : la demande est un état `MutableState<Long?>` détenu par
MainActivity (survit aux onglets), consommé UNE fois par HomeScreen ;
les extras sont retirés de l'intent après lecture (un recreate ne doit
jamais rouvrir une notification traitée) ; et un tap notification quand
l'app est déjà ouverte ouvre désormais aussi le dialog (avant :
silencieusement perdu).
- Tests : nouveau test UI instrumenté `LogDoseRequestTest` (ouverture 1× +
plus jamais après aller-retour d'onglets) + vérification émulateur.
### Ajouté — la notification de rappel saute un créneau déjà honoré
- **Demande** : si la dose prévue du jour est saisie AVANT l'heure de
rappel, la notification ne doit pas sonner pour une prise déjà faite ; le
rappel suivant repart au créneau suivant (idem pour le quotidien des
oraux sans Posologie).
- **Implémentation** : garde au moment du déclenchement —
`PharmacokineticEngine.hasDoseLoggedOnDay` (pur, jour calendaire LOCAL,
testable) consultée par `ReminderReceiver` : dose loguée aujourd'hui →
notification sautée (AppLog) + re-programmation du créneau suivant dans
tous les cas. Robuste quel que soit le moment de la saisie.
- Tests : 6 tests JVM (`ReminderLoggedDayTest` : même jour, veille,
frontière minuit, autre traitement, liste vide, mélange) + vérification
A/B sur émulateur (EEn dose-loguée-ce-jour → 0 notif ; Fluoxetine sans
dose → 1 notif).
Vérifié : **259 tests unitaires** (dont régression n°6 sur données réelles)
+ **14 tests UI** + lint vert ; web miroir : **180 tests + E2E verts**
(`check.sh`). Version web alignée au moment de la release (checklist 4.bis).
## [1.9.8] — 2026-09-22 (versionCode 45)
### Ajouté — tests UI Compose, signature release dédiée, README anglais
- **13 tests UI instrumentés** (navigation 5 onglets, dialog « Nouveautés »
— épinglage du fix v1.3.2 « ne réapparaît pas après dismiss » —, sections
Paramètres, version installée = BuildConfig : le bug #46 « versions jamais
bumpées » ne peut plus revenir silencieusement). Déterministes : permission
de notifications pré-grantée, zéro dépendance aux données, indépendants de
la langue FR/EN. Exécution : `./gradlew connectedDebugAndroidTest`.
- **Signature release dédiée, prête à l'emploi** : `scripts/make-release-keystore.sh`
crée le keystore + `keystore.properties` (gitignorés) ; `assembleRelease`
l'utilise dès qu'il existe (sinon clé debug inchangée). Migration
téléphone documentée (export JSON → désinstallation → réinstallation →
réimport). Aucun changement pour l'utilisatrice tant que la bascule n'est
pas faite.
- **README anglais** ([README.en.md](README.en.md)) — lien FR/EN en tête de
chaque fichier ; la doc de développement reste en français.
### Modifié — écran Paramètres découpé (maintenance)
- `SettingsScreen.kt` (840 lignes) devient un orchestrateur de 377 lignes +
11 cartes thématiques dans `ui/screens/settings/` (langue, calibration,
modèle T, seuils, fuseau, rappels, sauvegarde, auto-backup, logs, à propos).
**Comportement et rendu strictement identiques** (déplacement pur) ;
validé par les tests UI + smoke test RELEASE sur émulateur (toutes les
sections, version 1.9.8, 0 crash — anti-bug #64).
- `ChartScreen` reste volontairement entier : sa garde de source (#54)
exige le bloc de gestes dans ce fichier.
Vérifié : 249 tests unitaires + 13 tests UI verts, lint vert, APK debug +
release (2,7 Mo) buildés, smoke test release complet (accueil, 5 onglets,
Paramètres, éditeur de traitement, dialog de dose — 0 crash).
## [1.9.7] — 2026-09-20 (versionCode 44)
### Corrigé — la suggestion de prise de sang « change tout le temps » à chaque injection
- **Remontée** : « la suggestion de prochain test sanguin change tout le
temps à chaque injection si l'injection n'est pas faite pile à la même
heure ou si elle est faite 30 minutes plus tard que la précédente ».
- **Cause** : la règle de stabilisation v1.8.1 comparait l'écart inter-doses
**EXACTEMENT** à l'écart précédent (au ms près) — un log à 12:30 au lieu
de 12:00 cassait le régime, repoussant la stabilisation de 5 × t½ à
CHAQUE injection : le creux recommandé fuyait vers l'avant sans jamais
se fixer.
- **Fix (tolérance)** : chaque écart doit rester dans **l'intervalle de
Posologie ± 24 h** (`GAP_TOLERANCE_MS` — « je m'injecte le même jour, à
l'heure près »). La fenêtre se réfère à l'INTERVALLE THÉORIQUE, pas à
l'écart précédent : des logs à 6,8 j puis 7,2 j ne se déstabilisent plus
en cascade. Un vrai changement de créneau (2 j au lieu de 7 j) reste
hors fenêtre et réinitialise, comme avant. Le créneau affiché se cale
toujours sur la dernière dose réelle (voulu).
- Tests : le test v1.8.1 « interval change » ré-épinglé (NOW+21 j) + 2
nouveaux (flou d'heure ± 23 h → PAS de reset ; écart 25 h → reset).
249 tests verts + lint. Web miroir v1.9.7 (12 tests lab-timing).
## [1.9.6] — 2026-09-20 (versionCode 43)
### Modifié — la carte de l'Accueil liste TOUTES les prochaines prises
- **Remontée** : « si une injection et une prise d'un autre médicament ont
lieu en même temps… le rappel ne montre que la prise qui vient, non
toutes les prises du jour » — avec une prise quotidienne (CPA), le
rappel de l'injection hebdomadaire (EEn) « n'arrive qu'après celui de la
CPA, du coup je ne le vois quasiment pas ».
- **Fix** : la carte « Prochaine dose » devient « **Prochaines doses** »
(titre pluriel dès 2 lignes) — **une ligne par traitement, TRIÉES par
prochaine prise** ; la 1ʳᵉ ligne (la plus proche) reste mise en avant.
Format de ligne inchangé (`Xj Yh · ven. 25 10:00 (nom)`, `XhMM · HH:mm`,
`N min`). Le CPA se lit désormais AU-DESSUS de l'EEn du samedi, jamais à
sa place.
- Les NOTIFICATIONS de rappel sont inchangées (une alarme par traitement
— le problème était purement l'affichage de l'Accueil).
- Moteur : helper PUR `nextDoseLine` (HomeScreen.kt, locale/zone
paramétrables, template jours localisé) — 3 tests (NextDoseLineTest).
247 tests verts + lint.
## [1.9.5] — 2026-09-20 (versionCode 42)
### Corrigé — revue de code post-v1.9.4 (audit lecture-seule des deux dépôts)
- **#67 — les ticks X ignoraient le fuseau choisi** : `drawXLabels` formatait
les labels DANS le fuseau choisi mais appelait `xLabelTicks` SANS le
passer — les ticks étaient alignés sur le fuseau de l'appareil (heures
décalées dès que les deux divergent, contredisant le fix #55). Fix :
zone passée + **pas avancé par Calendar** (un `+24 h` fixe dérivait d'1 h
aux transitions DST) ; test DST épinglé (Europe/Paris, 29/03/2026).
- **`EstrannaiseCloud.compute` : paramètre `nowMs` mort supprimé** (jamais
lu — le KDoc prétendait une sémantique que le code n'avait pas) ;
appelants + tests mis à jour.
### Amélioré — perf (suite de l'optimisation v1.8.2)
- **Contexte moteur pré-calculé** (`E2Context` + `prepareE2Context`) : le
tracé labs appelle `e2At` par point de grille (720 pts) — chaque appel
reconstruisait les maps internes (groupBy/cutoffs/Bateman), diluant
l'optimisation v1.8.2. Préparé UNE fois par courbe labs ET par
calibration par ester ; **résultat bit-identique** (244 tests verts).
- **Canvas** : Paints texte MÉMOÏSÉS par (couleur, taille, align, gras)
(une alloc par extremum/label × frame → une par clé) ; `detectExtrema`
mémoïsé par série (scan O(n) par frame → par changement de séries) ;
formatters X/marqueurs mémoïsés (SimpleDateFormat par frame → par
fuseau).
### Documenté — commentaires et docs rattrapés après les refontes
- Commentaires à jour avec le code : `PKPresets.kt` (l'ODS n'existe plus
au runtime), `Treatment.kt` (ESE analytique 6 esters),
`LabTimingTest.kt` (t½ analytique), doc §7.2/§7.3 (asset historique,
cutoff = 10 × t½ depuis v1.9.0).
- Règle anti-#59 ré-appliquée : labels de modèle via `modelLabelRes` (le
`when` inline de l'éditeur violait la règle) ; literals « ESE »/« TFS »/
« WHS » remplacés par `PKModels.*` (ChartScreen/CurveChart) ;
`scheduler2` supprimé (le scheduler de l'écran existe déjà).
## [1.9.4] — 2026-09-19 (versionCode 41)
### Ajouté — les presets du modèle ESE couvrent ses 6 esters injectables
- **Remontée** : depuis l'ajout du modèle source Estrannaise (v1.9.0), les
presets de création n'offraient toujours que EV/EU/EEn — « les presets
n'ont pas été mis à jour lors de l'ajout du modèle source pour ESE ».
- **Presets (`PKPresets.kt`, 25 entrées)** : 3 presets ESE ajoutés au
même niveau que les autres — EC huile (5 mg IM), EB (1 mg IM), EUCS
undécylate suspension cristalline (10 mg SC, ester exclusif à ce fit).
Champs Bateman informatifs = t½ terminales des fits (pas utilisés au
calcul — le dispatch passe par la forme close).
- **Éditeur (`TreatmentEditorScreen.kt`)** : la liste des esters suit la
couverture RÉELLE du modèle choisi (`Esters.choicesForModel` — même
liste que l'override du DoseDialog) : ESE liste ses 6 esters
injectables directement à la création. Au changement de modèle, si
l'ester courant sort de la couverture (ex. EUCS + TFS), il rebascule
sur EV — garde anti « 0 pg/mL ».
- **Tests** : `PKPresetsTest` (nouveau, 6 tests — miroir
`tests/presets.test.js` web) : couverture des 6 esters ESE, unicité,
TFS (7) / WHSAH (6) inchangés, garde « aucun preset injectable hors
couverture de son modèle », plausibilité des t½ informatives.
243 tests verts + lint.
- **Doc** : doublon de la section §7.12 supprimé (collé deux fois).
## [1.9.2] — 2026-09-19 (versionCode 40)
### Corrigé — le nuage d'incertitude exigeait un traitement STOCKÉ en ESE
- **Bug remonté** : activer Estrannaise dans le graphique puis le nuage
n'affichait rien tant qu'aucun traitement n'était **stocké** avec le
modèle ESE — « la seule méthode trouvée était de mettre un traitement en
cours sur le modèle ESE, mais ça ne devrait pas être un prérequis ».
- **Cause** : le filtre du nuage testait le `pkModel` **STOCKÉ** du
traitement, alors que la courbe ESE affichée redessine toutes les doses
E2 avec `modelOverride = "ESE"` (peu importe le modèle stocké) — courbe
et nuage n'utilisaient pas la même définition de « quelles doses sont
tracées en ESE ».
- **Fix** : le nuage couvre **toutes les doses E2 à profil injectable dont
l'ester effectif (override compris) est couvert par le fit Estrannaise**
— indépendamment du modèle stocké. L'exclusivité ESE reste portée par le
chip (activable seulement si ESE est affiché). L'oral Bateman reste hors
nuage (pas d'ester échantillonnable). Les traitements inactifs sont
inclus (§6.bis : la courbe ESE les trace aussi).
- 2 tests réécrits (237 au total / 207 sans les données locales) :
« doses d'un traitement TFS couvertes quand ESE est affiché » (le fix,
épinglé des deux plateformes) ; oral seul → vide (conservé).
- Validé émulateur §16.ter sur le profil réel (EEn stocké TFS, ESE
activé) : nuage visible autour de la courbe ESE, 0 crash.
## [1.9.1] — 2026-09-19 (versionCode 39)
### Corrigé — le nuage d'incertitude entoure maintenant la courbe Estrannaise calibrée
- **Bug remonté** : le nuage ne s'activait « que autour du tracé, pas autour
du modèle Estrannaise » — la courbe ESE affichée est **calibrée**
(scalePerEster des labs) mais le nuage était tracé **brut** : les deux
flottaient à des échelles différentes.
- **Fix** : le nuage reçoit le MÊME `scalePerEster` que la courbe ESE
(autoByModel ESE) — il entoure désormais la courbe calibrée (Android +
web en miroir).
- 1 nouveau test : nuage calibré ×2 = nuage brut ×2 (le nuage suit la
calibration).
### Note — pourquoi le taux affiché a bougé depuis v1.8.2 (+4 pg/mL)
- v1.8.2 a corrigé l'**arrondi de la médiane de calibration** (troncature
flottante → arrondi au plus proche) : une échelle dont la médiane brute
tombait à 0,8999… était tronquée en 0,89 au lieu de 0,90. Avec le modèle
ESE analytique (v1.9.0) déplaçant ce dernier bit, le piège est devenu
visible : les échelles auto-calibrées ont bougé d'un cran d'affichage →
le niveau estimé a monté d'environ 1 % (+4 pg/mL sur ~320).
- **Le niveau actuel est le CORRIGÉ** (l'arrondi est plus juste que la
troncature). Le web applique la même règle depuis v1.9.1 (harmonisation
des niveaux entre plateformes).
## [1.9.0] — 2026-09-18 (versionCode 38)
### Modèle Estrannaise ANALYTIQUE (abandon de l'ODS) + nuage d'incertitude MCMC
- **Le modèle ESE n'est plus les tables horaires ODS** : il est désormais la
**forme close 3C publiée par estrannaise.js** (les sources originelles
dont l'ODS n'était que l'échantillonnage horaire) — fidélité épinglée :
**RMS 0,00** vs les tables sur 0→200 j, écart de pic ≤ 0,1 %.
- **Décision** (utilisatrice) : « tous les modèles ont maintenant leurs
sources originelles — on abandonne complètement les liens avec le fichier
ODS ». Concrètement : plus aucune lecture des tables au runtime
(`concentrationOfDose` dispatche ESE → forme close ; `cutoffHours` ESE →
10 × t½ terminale analytique ; l'asset `pk_profiles.json` quitte l'APK,
−550 Ko, et ne sert plus qu'aux tests de fidélité).
- **6 esters injectables couverts** (EV, EU, EEn, EC, EB + **EUCS**,
undécylate suspension cristalline, exclusif à ce fit) — l'ancien ESE
n'en couvrait que 3. TFS reste à 7 (liste explicite : EUCS n'a pas de
V3C TFS), WHSAH à 6.
- **NUAGE D'INCERTITUDE (exclusif ESE)** : Estrannaise publie le posterior
MCMC de ses paramètres (313 échantillons/ester, asset ≈ 48 Ko) — chip
`Nuage` (off par défaut, activable à volonté, actif SEULEMENT si ESE est
affiché, coupure automatique si ESE est éteint) → nuage diffus de 32
courbes du posterior montrant la plage d'imprécision du modèle, comme
sur le site estrannaise. Dessiné en alpha faible sous les courbes, hors
échelles/labels/extrema.
- t½ terminale ESE désormais **analytique** (ln2/min(k1,k2,k3)) — la
recommandation de prise de sang n'estime plus la t½ par pente de table.
- **11 nouveaux tests** (234 au total / 204 sans les données locales) :
fidélité (RMS, pics ±0,2 %), cas dégénérés (taux égaux → limites sans
NaN), MCMC 313 × 6, nuage (32 courbes à dispersion réelle, fenêtre,
exclusivité ESE, gardes), choicesForModel ESE=6, t½ analytique.
- **Porté web** (web v1.9.0, versions sync) : `estrannaise-models.js` +
`estrannaise-cloud.js` miroirs, nuage canvas, fetch MCMC au démarrage
(l'ODS web quitte aussi le runtime), 11 tests miroirs + E2E nuage.
- Validé émulateur §16.ter (APK release, seed réel v1.7.0) : courbe ESE
analytique affichée, chips Extend/Cloud présents, recommandation
de prise de sang fonctionnelle, 0 crash.
## [1.8.2] — 2026-09-17 (versionCode 37)
### Maintenance — performance du moteur + fraîcheur de l'interface
Aucune nouvelle fonctionnalité : session d'audit complet (code + doc), avec
les optimisations et corrections sûres qui en sont sorties.
### Performance du moteur PK
- `e2At`/`computeCurve` : `cutoffHours` et les paramètres Bateman
(bisection 50 itérations) ne sont plus recalculés à chaque (point ×
dose × traitement) — pré-calculés par traitement, et Bateman devient
**paresseux** (inutile pour les traitements à profil). Le chemin le plus
chaud (tracé labs : ~720 points × N × D appels de cutoff par refresh)
s'allège d'un facteur P × T. Doses pré-groupées par traitement (fin des
scans complets). **Résultat identique** (ordre de sommation préservé —
223 tests verts inchangés).
- `AppLog` : purge du buffer sans recopie à chaque ligne loggée.
- `ChangelogHelper` : Regex précompilées (dialog « Nouveautés »).
- `CurveChart` : SimpleDateFormat créé hors boucle de dessin.
### Corrigé — fraîcheur de l'interface
- **Accueil** : éditer un scaleFactor ou le modèle T rafraîchit la courbe
immédiatement (clés du producteur incomplètes — l'ancien trio attendait
le tick de 60 s).
- **Graphiques** : éditer la VALEUR d'une analyse rafraîchit les courbes
(la clé n'était que le NOMBRE d'analyses).
### Maintenance — code & documentation
- Code mort supprimé (Repository/DAO sans appelants, `currentLevel`,
variable et scheduler dupliqués), imports morts/redondants nettoyés,
médiane de calibration factorisée (4 copies → 1 helper),
qualifications raccourcies, en-têtes/KDoc obsolètes mis à jour.
- Documentation : ~35 corrections (compteurs de tests obsolètes, TOC et
ancres réparées, inventaire des suites de tests à jour et fusionné,
titres CHANGELOG doublés supprimés, versions « à jour v1.2.0 » /
« Gradle 8.9 » / « API 34 » corrigées, fautes).
## [1.8.1] — 2026-09-17 (versionCode 36)
### Corrigé — la stabilisation ignorait les changements récents (dose/ester/intervalle)
- **Bug remonté** : la recommandation disait « stabilisée depuis février »
alors que l'ester, le dosage ET la posologie venaient de changer —
l'ancien calcul partait de la **1ʳᵉ dose du traitement** (proxy aveugle
aux changements récents).
- **Fix** : le régime courant = la **séquence terminale de doses** à
(ester effectif, dose mg, **écart inter-doses**) constants — tout
changement récent réinitialise la stabilisation : la recommandation saute
au premier creux **après** la stabilisation du nouveau régime (règle des
~5 demi-vies conservée, intervalle comparé exactement). La carte porte
désormais l'**ester effectif** de la dernière dose (override compris).
- **Nouveau : suggestion aussi sur l'Accueil** (demande) — carte compacte
(creux daté + créneau associé), avec la mention de stabilisation
seulement si le régime n'était pas déjà stable.
- Validé émulateur sur l'export réel : la carte passe de « stabilisé depuis
le 06/02 » (v1.8.0) à « **EEN pas stabilisé avant le 29/09 — premier
creux fiable après ton dernier changement** », creux recommandé 27/09
22:43, 0 crash.
- 3 nouveaux tests (223 au total / 193 sans les données locales) :
changement de dose / d'intervalle / d'ester → réinitialisation épinglée.
- **Web** : même fix en miroir + encart Accueil (web v1.8.1, versions sync).
## [1.8.0] — 2026-09-17 (versionCode 35)
### Prochaine prise de sang recommandée (page Analyses)
- **Nouvelle carte sur la page Analyses** : « Prochaine prise de sang
(suggestion) » — l'app déduit de ses courbes quand refaire une analyse :
**au creux estimé, juste avant l'injection suivante** (le creux est le
moment le plus informatif et comparable ; l'heure exacte du creux calculé
est affichée, avec la mention « ou simplement la veille du créneau »).
- **Condition de stabilité** (choix v1.8.0) : un creux n'est interprétable
que si l'ester est stabilisé — règle des **~5 demi-vies terminales**
(≈ 97 % de l'équilibre). L'app **saute donc au premier creux stabilisé**
après un changement d'ester/dose/rythme, et la carte l'explique
(« stabilisé depuis le … (~5 demi-vies) » / « c'est le premier creux fiable
depuis ton dernier changement »). Le modèle Estrannaise lit sa demi-vie
dans les tables (nouveau `PKProfileStore.terminalHalfLifeDays`), TFS/WHSAH
la calculent analytiquement.
- **Périmètre honnête** : la carte n'apparaît que s'il existe un traitement
E2 **injectable actif avec une Posologie** (sinon pas de creux prévisible) ;
**et si un injectable actif est SANS Posologie, la page affiche une invite**
« renseigne une Posologie pour recevoir des recommandations » (demande
v1.8.0). Créneaux déjà mesurés jamais re-recommandés (prise en compte des
analyses existantes). Calcul hors UI thread + rafraîchi chaque minute ;
disclaimer estimation visible sur la carte.
- **11 nouveaux tests** (220 au total / 190 sans les données locales) :
LabTimingTest (8 : creux avant créneau, saut de stabilisation, stabilisé,
prise récente → creux suivant, sans Posologie → null, oral → null,
invite, Estrannaise lit sa t½ dans la table) + régression n°5 étendue
(recommandation cohérente sur l'export réel).
- **Porté web** (web v1.8.0, versions sync) : `js/pk/lab-timing.js` miroir +
encart + scénario E2E.
- Validé émulateur §16.ter (APK release, seed réel v1.7.0) : carte rendue —
« creux 20/09 10:37, juste avant l'injection EEN du 20/09 11:20, EEN
stabilisé depuis le 06/02 » — 0 crash.
## [1.7.1] — 2026-09-16 (versionCode 34)
### Corrigé — une seule note sur deux s'affichait dans les Analyses
- **Bug remonté** (identique côté web) : dans une prise de sang E2+T, quand
les deux entrées portent des notes **distinctes**, une seule s'affichait
dans l'écran Analyses — l'autre était bien **conservée** (export/import
l'avaient) mais perdue à l'affichage.
- **Cause** : l'affichage ne prenait que la première note non vide du groupe
(hypothèse historique « toutes identiques en pratique » — vraie à l'époque
du dialog de création, fausse depuis l'édition unitaire E2/T).
- **Fix** : `labNotesForDisplay` (helper pur, testé) — notes distinctes →
une ligne par note **préfixée du marqueur** (« E2 : … » / « T : … ») ;
note identique sur les deux entrées → une seule ligne (pas de doublon) ;
notes vides ignorées ; une seule note → brute (comportement v1.0 gardé).
- **Épinglé par** : 5 nouveaux tests LabsGroupingTest + **régression n°5
data-driven sur le nouvel export réel** (v1.7.0 : la dernière prise
porte exactement ce cas — deux notes distinctes E2/T, les deux
affichées). Validé émulateur sur l'APK release seedé avec cet export.
- **Web** : même fix en miroir (web v1.7.1, versions sync).
## [1.7.0] — 2026-09-13 (versionCode 33)
### Sauvegarde automatique quotidienne (opt-in)
- **Nouvelle option Paramètres → « Sauvegarde automatique quotidienne »**
(désactivée par défaut, en PLUS de l'export manuel qui reste inchangé) :
chaque jour, l'app écrit une sauvegarde JSON complète (MÊME format que
l'export manuel — importable tel quel) dans le **dossier que tu choisis**
(ex. un dossier Owncloud synchronisé → le backup atterrit dans ton cloud).
- **Aucune permission de stockage** : le dossier est choisi UNE FOIS via le
sélecteur système (SAF) — la permission persiste (survit au reboot), tu
peux la révoquer à tout moment dans les réglages système. 100 % local :
l'écriture va uniquement dans CE dossier.
- **Rétention configurable** (1–30 copies, défaut 7) : chaque run crée un
NOUVEAU fichier horodaté `hormonetrack-auto-20260913-1830.json` (jamais
d'écrasement — les N dernières copies saines restent disponibles) et les
plus vieilles sont supprimées. **Tes exports manuels, tes logs et tout
fichier étranger du dossier ne sont JAMAIS touchés** (rétention pure,
épinglée par tests).
- **Statut visible** : « Dernière sauvegarde : OK/ÉCHEC (date) » dans
Paramètres + journalisation AppLog complète (diagnostic à distance).
À l'activation, un **premier backup est écrit immédiatement** (pas
d'attente de 24 h) ; ensuite WorkManager (24 h, survit au reboot, gère
le Doze).
- 9 nouveaux tests (200 au total / 177 sans les données locales) : noms
horodatés (règle anti-#48 : signature LocalDateTime obligatoire), garde
de rétention (jamais de suppression hors auto-backups reconnaissables,
keep clampé ≥ 1 — `keep=0` ne supprime jamais la dernière copie).
- Validé sur émulateur (§16.ter, APK release + recette standard) : dossier
choisi via le sélecteur SAF, fichier `hormonetrack-auto-…json` écrit,
rétention appliquée, 0 crash.
### Unités des axes du graphique
- **pg/mL** (E2, axe gauche) et **ng/mL** (T, axe droit) affichées au sommet
des colonnes de labels du graphique de simulation — remontée : « on n'a
jamais ajouté les unités des mesures dans les axes » (des nombres nus
depuis la v1.0). Portées aussi côté web (v1.7.0 web, versions sync).
## [1.6.0] — 2026-09-12 (versionCode 32)
### « Tracé labs » prolongé au-delà du dernier lab
- **Nouveau chip `Prolonger`** (à côté de `Tracé labs`, **off par défaut**,
sans effet — donc désactivé — tant que Tracé labs est off) : la courbe
ancrée sur tes prises de sang se PROLONGE désormais au-delà du dernier lab,
au lieu de s'arrêter là (limite v1.5.0 : justement la période la plus
récente était invisible).
- **Recette de la prolongation** : `courbe(t) = M(t) × ρ_dernier lab` où
M(t) = le modèle PK brut (porte TOUTE la dynamique : Tmax, queues,
accumulation **et les doses loguées après le dernier lab** — une nouvelle
injection, même à un autre ester, fait repartir la courbe en pic) et
ρ = le ratio corps-réel ÷ modèle observé au dernier lab, supposé constant.
Demande couverte : simuler à partir des labs précédents, du dosage et du
type d'ester injecté, et de l'évolution classique de l'ester.
- **Horizon honnête** : la prolongation s'arrête quand le MODÈLE lui-même
s'éteint (dernière dose + coupure moteur = 10 t½ terminales V3C/WHS, fin
de table ODS, 30 t½ Bateman) — jamais de ligne à zéro inventée au-delà.
- **Lisibilité** : la partie prolongée est dessinée SÉPARÉMENT (même rose,
atténué) avec sa propre légende — « estimation, plus ancrée » : la partie
mesurée (qui passe par les labs) ne peut pas se confondre avec la partie
simulée.
- **Avertissement visible** (demande) : tant que la partie prolongée est
affichée, un texte sous la légende rappelle que c'est une **simple
simulation sans garantie de correspondre au réel**, extrapolée à partir
de tes résultats de laboratoire — qui peuvent **eux-mêmes être erronés**
— et qu'il faut se fier à la prochaine prise de sang, pas à cette courbe.
- **Garde anti-ratés inchangée** : seuls les labs SIGNIFICATIFS ancrent
(leçon #61) — un lab tardif hors fenêtre d'action ne prolonge pas la
courbe et n'est jamais traversé.
- **Limites documentées** (cf doc de développement §7.10.bis) : ρ est
constant (pas d'extrapolation de pente — elle divergerait sans base
physiologique) ; après un changement d'ester, l'amplitude reste calibrée
par le ρ de l'ancien ester (même limite que la calibration classique) ;
les doses PRÉVISIONNELLES ne participent pas (la projection reste la
série Prévision).
- **8 nouveaux tests** (191 au total / 168 sans les données locales) :
identité ρ constant + continuité de suture, horizon = cutoff du
traitement, montée après une dose EV loguée après le dernier lab,
flag off = comportement v1.5.0 bit-compatible, garde #61 sur la
prolongation, gardes du helper d'horizon, `lastAnchorMs`.
- **Validé sur émulateur** (recette §16.ter, APK release + données
réelles seedées) — et heureusement : la validation a TROUVÉ le bug
#64 (courbe vide en release seule, inlining destructeur de R8 full
mode sur `LabTrajectoryModel`, cf doc §14). FIX : garde R8 explicite ;
re-validé en release : ancrée, prolongée, avertissement, 0 crash.
## [1.5.0] — 2026-09-11 (versionCode 31)
### « Tracé labs » — courbe ancrée sur tes prises de sang
- **Nouvelle courbe superposable** (chip `Tracé labs`, **off par défaut**)
: `courbe(t) = M(t) × ρ(t)` — le modèle PK garde la forme biologique
(Tmax, queues, accumulation) et l'amplitude est recalée sur CHAQUE prise
de sang : la trajectoire réelle que tes labs tracent, avec la dynamique
du modèle entre les points.
- **Passage exact garanti** sur chaque lab ; transition continue
(log-linéaire) d'un lab au suivant — jamais de saut vertical.
- Garde anti-ratés : un lab qui ne « gouverne » plus la prédiction du
modèle (< 15 % du max, cf leçon #61) n'est PAS forcé-traversé — une
dose de test de janvier ne crée pas de zigzag.
- **E2 uniquement** (unité pg/mL) ; fenêtre = entre le 1er et le dernier
lab significatif — rien avant/après : la courbe s'arrête à ta dernière
prise de sang (honnête, pas d'extrapolation).
- Indépendante de la calibration (elle est SA propre référence) et
n'entre JAMAIS dans l'accueil/les seuils d'alerte/les rappels — une
courbe de comparaison, pas une estimation d'action.
- Couleur dédiée rose foncé pointillé, hors pics/creux.
### Améliorations UI
- La rangée de chips du graphique défile horizontalement (le 4ᵉ chip
était coupé hors fenêtre sur écrans étroits — constat émulateur v1.5.0).
## [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)
### 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 / 391)
→ 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)
### 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<String, Double>` + `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)