2545 lines
204 KiB
Markdown
2545 lines
204 KiB
Markdown
# Documentation de développement — HormoneTrack
|
||
|
||
> Doc de référence pour toute future session (humaine ou IA) : contexte, décisions,
|
||
> architecture, maths, build, tests, bugs corrigés, montre, évolutions.
|
||
> Projet : `~/projects/HormoneTrack` — voir aussi [README.md](../README.md),
|
||
> [GUIDE_INSTALLATION.md](GUIDE_INSTALLATION.md), [MONTRE-GADGETBRIDGE.md](MONTRE-GADGETBRIDGE.md).
|
||
|
||
---
|
||
|
||
## Table des matières
|
||
|
||
1. [Contexte & objectifs](#1-contexte--objectifs)
|
||
2. [Historique du projet](#2-historique-du-projet)
|
||
3. [Stack & versions (épinglées — à jour v1.10.0)](#3-stack--versions-épinglées--à-jour-v1100)
|
||
4. [Environnement de build (cette machine)](#4-environnement-de-build-cette-machine)
|
||
5. [Architecture générale](#5-architecture-générale)
|
||
6. [Modèle de données (Room)](#6-modèle-de-données-room) + [6.bis Sémantique isActive](#6bis-sémantique-isactive-v124--drapeau-administratif-jamais-un-filtre)
|
||
7. [Moteur pharmacocinétique](#7-moteur-pharmacocinétique) + [7.8 Modèle TFS V3C](#78-modèle-tfs-v3c-v140--pktransfemsciencemodelskt) + [7.9 Modèle WHSAH](#79-modèle-whsah-v146--pkwhsahmodelskt) + [7.10 « Tracé labs »](#710--tracé-labs--v150--pklabtrajectorymodelkt) + [7.10.bis Prolongation](#710bis-prolongation-au-delà-du-dernier-lab-v160--pklabtrajectorymodelkt) + [7.11 Prise de sang recommandée](#711-recommandation-de-prochaine-prise-de-sang-v180--pklabtimingkt) + [7.12 Estrannaise analytique](#712-modèle-estrannaise-analytique--nuage-mcmc-v190--pkestrannaisemodelskt-pkestrannaisecloudk t)
|
||
8. [Tests unitaires](#8-tests-unitaires) + [8.bis Données de test réelles : HORS dépôt](#8bis-données-de-test-réelles--hors-dépôt-local-test-data) + [8.ter Tests UI Compose](#8ter-tests-ui-compose-appsrcandroidtest-v198)
|
||
9. [Système de rappels](#9-système-de-rappels) + [9.bis Alertes de seuil](#9bis-alertes-de-seuil--reminderalertnotifierkt--worker-v142)
|
||
10. [UI & navigation](#10-ui--navigation)
|
||
11. [Graphiques (CurveChart)](#11-graphiques-curvechart)
|
||
12. [i18n FR/EN](#12-i18n-fren)
|
||
13. [Sauvegarde JSON](#13-sauvegarde-json) + [13.bis Auto-backup quotidien](#13bis-sauvegarde-automatique-quotidienne-v170--databackupautobackupworkerkt)
|
||
14. [Bugs corrigés (historique complet — à ne pas réintroduire)](#14-bugs-corrigés)
|
||
15. [Comment régénérer l'asset pk_profiles.json](#15-comment-régénérer-lasset-pk_profilesjson)
|
||
16. [Workflow build / test / install / git](#16-workflow-build--test--install--git) + [16.bis Releases Gitea](#16bis-releases-gitea-avec-apk-téléchargeable) + [16.quater Signature release : keystore dédié](#16quater-signature-release-keystore-dédié-v198) + [16.ter Recette : test sur émulateur](#16ter-recette--test-manuel-sur-émulateur-reproductible--v134)
|
||
17. [Montre : Gadgetbridge & options](#17-montre--gadgetbridge--options)
|
||
18. [Espace disque & coûts](#18-espace-disque--coûts)
|
||
19. [Limites connues](#19-limites-connues)
|
||
20. [Idées d'évolution (Phase 2+)](#20-idées-dévolution)
|
||
21. [Checklist de test manuel](#21-checklist-de-test-manuel)
|
||
|
||
---
|
||
|
||
## 1. Contexte & objectifs
|
||
|
||
Utilisatrice : femme trans, THS (thérapie hormonale), injections d'estradiol (esters
|
||
EV/EU/EEn, switchables) ± anti-androgènes. Elle tient déjà un suivi rigoureux dans
|
||
**LibreOffice Calc** (`Estrogen.ods`, cf §7.1) avec deux modèles PK : **Estrannaise
|
||
(EstraNase)** et **Transfem Science**. L'app doit reproduire fidèlement ces modèles.
|
||
|
||
Montre : **Huawei Watch GT 3 (HarmonyOS 4.0.0.120)** = *Lite Wearable*, pas d'apps
|
||
Android, apps tierces au poignet quasi impossibles (cf §17). Utilisatrice équipe de
|
||
**Gadgetbridge** (FOSS) sur son téléphone → v1 = **app téléphone + notifications miroir
|
||
sur la montre** via GB (ou Huawei Health).
|
||
|
||
> **Développement IA-assisté** : le code a été produit avec un assistant IA ;
|
||
> la contribution humaine = feedback continu, retours utilisateur (tests réels
|
||
> sur téléphone, bugs avec exports), suggestions et validation des releases
|
||
> (détail §2 — factuel, session par session).
|
||
|
||
Fonctionnalités v1 :
|
||
- Courbes estimées heure par heure : E2 (pg/mL) + T (ng/mL) — 24 h / 7 j / 30 j
|
||
- Deux modèles PK du `.ods` (Estrannaise / TFS) pour injections EV/EU/EEn ;
|
||
Bateman paramétrable pour gel/patch/oral
|
||
- Log des doses (date/heure exacte, mg, **ester par injection**)
|
||
- Labs (E2/T/PRL) + **calibration** (facteur d'échelle + calibration k du modèle T)
|
||
- Rappels quotidiens, actions « Pris » / « Reporter 1 h » dans la notification
|
||
- Export/Import JSON, FR/EN, 100 % local
|
||
|
||
## 2. Historique du projet
|
||
|
||
| Date | Événement |
|
||
|---|---|
|
||
| 29 sept. 2026 (session v1.12.0) | **Fix #70 + 2 améliorations Doses** (demandes). (a) **#70 — inactif à Posologie projetait encore** : `generateForecastDoses` (source unique des créneaux) ne vérifiait pas l'archivage, et l'horizon du chip Prévision incluait les Posologies INACTIVES. FIX : garde `!isActive` dans le moteur + horizon/chip sur les ACTIFS — §6.bis inchangé (historique toujours simulé, seul le FUTUR s'arrête, comme les rappels v1.4.0). **Miroir web** (pk-engine.js + chart.js). 3 tests JVM + 3 web. Émulateur : seed modifié (2 inactifs à Posologie 7 j), Prévision on/off 0 crash — leçon uiautomator : l'attribut `selected` des FilterChip ne reflète PAS la sélection Compose (vérifier par la LÉGENDE attendue, comme dit la recette §16.ter). (b) **Icônes de forme de prise** dans Doses (demande) : `routeIcon` pur — pilule/seringue (IM+SC, teinte tertiary pour SC)/goutte/sparadrap/compte-gouttes ; ⚠️ `MedicationLiquid` N'EXISTE PAS dans icons-extended 1.7.8 (résolu hors BOM) → `Colorize` pour OTHER (vérifié au jar). (c) **Marqueurs « prise de sang le même jour »** : `labMarkersForDose` pur (jour calendaire LOCAL, lab à l'heure exacte = APRÈS — convention documentée) → encart latéral 🧪↑/🧪↓ sur la ligne ; miroir web émojis + title. 7 tests JVM `DosesExtrasTest` + 10 web `doses-extras.test.js` (⚠️ helper web `makeDose(ts, amount, overrides)` — le 1ᵉʳ arg est le TIMESTAMP). Validé émulateur : content-desc des icônes (IM injection/Oral) + marqueur « Blood test later the same day » sur la dose du 10 sept. 274 tests JVM + 14 UI + lint ; web 194 tests + E2E (check.sh). **Publication** : commit b3372a8 + tag v1.12.0 poussés sur les 2 Gitea, APK release (2,7 Mo) + debug (21 Mo) publiés et vérifiés par téléchargement (cloudyfy id 82, farewell id 1588590) ; web v1.12.0 sync (commit 4bdcd52 + tag, farewell = miroir serveur déjà répliqué). **⚠️ OUBLI de l'utilisatrice : les RELEASES web n'avaient pas été publiées depuis v1.9.7** (étape 4 du processus web sautée — tags seuls) → backfill le jour même : v1.10.0, v1.11.0, v1.12.0 sur cloudyfy ET farewell + **v1.9.5 backfillée sur farewell** (jamais publiée) ; zips vérifiés par téléchargement ; leçon documentée §10 web (« commit + tag » ≠ « release »). |
|
||
| 29 sept. 2026 (session v1.11.0) | **Page Traitements : inactifs regroupés en bas + atténués** (demande : « peu discernables — les placer à la fin, et peut-être les rendre plus distincts »). Helper PUR `treatmentsForDisplay` (tri STABLE : actifs d'abord, inactifs après, ordre relatif conservé DANS chaque groupe) + LazyColumn en 3 sections (actifs / en-tête `inactive_section` SEULEMENT si des inactifs existent / inactifs `dimmed = true`) + cartes inactives **alpha 0,55** en plus du badge (lisibles et tappables — l'édition d'un archivé doit rester possible). **Miroir web** : `treatmentsForDisplay` dans `js/data/models.js` (node-safe, pas de DOM) + rendu sections + `opacity:0.55` + clés i18n `inactive_section` FR/EN. Tests : **5 JVM** (`TreatmentsDisplayTest` : regroupement, stabilité, tout-actif, tout-inactif, vide) + **4 web** (`tests/treatments-display.test.js`) — pas de test UI instrumenté pour l'en-tête (dépendrait de données inactives, contre la convention data-indépendance §8.ter) : couverture JVM + validation émulateur (5 actifs en haut, en-tête + EV-old/EEn-old tout en bas, dumps tr1/tr2). 264 tests JVM + lint ; web 184 tests + E2E verts (check.sh). **Publication** : commit a0f3ac5 + tag v1.11.0 poussés sur les 2 Gitea, APK release (2,7 Mo) + debug (21 Mo) publiés et vérifiés par téléchargement (cloudyfy id 80, farewell id 1588588) ; web v1.11.0 sync (commit 0dd5fa7 + tag, farewell = miroir serveur déjà répliqué). |
|
||
| 28 sept. 2026 (session v1.10.1) | **Durcissement rappels — revue de code post-v1.10.0** (audit demandé : « optimisations ou problèmes, web et Android »). (a) **`launchMode="singleTop"`** : le combo `standard`+`CLEAR_TOP` recréait l'activité au tap notification app-ouverte et rendait le `onNewIntent` de la v1.10.0 inopérant (code mort) — désormais délivré SANS recreation (état UI préservé). (b) **`EXTRA_PLANNED_AT`** : le garde « créneau déjà honoré » évaluait le jour au moment du FEU — une alarme inexacte (`setWindow`/Doze) glissant 23:58→00:02 évaluait le mauvais jour ; l'instant planifié voyage dans l'intent (scheduleFor), le snooze le FORWARDE, le garde évalue le jour du CRÉNEAU ; vérifié émulateur (broadcast `planned_at` hier → notif sautée). (c) **`BootReceiver` consolidé** (dette §20.bis #2 FAIT) : `goAsync`+coroutine, `runBlocking` supprimés. (d) Style (import `mutableStateOf`) + audit doc (comptes 223 résiduels §14/#64 et §16.ter → 259 ; §9 et §20.bis à jour). Web NON concerné (miroir lab-timing laissé tel quel — boucle correcte et testée ; `importJson` par lots reste en dette). 259 tests JVM + 14 UI + lint verts. **Publication** : commit d531eca + tag v1.10.1 poussés sur les 2 Gitea, APK release (2,7 Mo) + debug (21 Mo) publiés et vérifiés par téléchargement (cloudyfy id 78, farewell id 1588586) ; validation §16.ter sur l'APK RELEASE AVANT tag (accueil + données, singleTop, planned_at A/B, 0 crash). |
|
||
| 27-28 sept. 2026 (session v1.10.0) | **2 fixes + 1 amélioration rappels** (remontées + nouvelles données réelles → 6ᵉ régression épinglée `backup-v1.9.8.json`). (a) **#68 — reco de prise de sang antérieure à la stabilisation** : le « creux » = minimum de la FENÊTRE ENTIÈRE tombait juste après l'injection PRÉCÉDENTE pour les esters à montée lente (EEn pic ~J+5 ≈ intervalle 7 j → creux d'absorption ~3 h post-injection) — date gouvernée par l'injection N−1 (antérieure à la stab) et « juste avant ton injection du … » faux de plusieurs jours (réel : creux 5 oct 02:32 < stab 7 oct 04:03). FIX : creux = niveau PRÉ-INJECTION du créneau (restaure le principe v1.8.0 documenté ; EV inchangé — le point pré-injection EST son min de fenêtre) ; **miroir web appliqué** (lab-timing.js). Tests : RegressionUserCase6Test data-driven dont balayage de `now` ±60 j (invariant « creux ≥ stab » à TOUT INSTANT) ; les 17 LabTimingTest passent inchangés. (b) **#69 — dialog fantôme** : tap notification → dialog ; le fermer + changer d'onglet + revenir sur l'accueil le ROUVRAIT à chaque fois (paramètre d'activité vs consommation dans les remember de Home, détruits à chaque sortie d'onglet — reproduit sur émulateur AVANT fix). FIX : `MutableState<Long?>` détenu par MainActivity, consommé UNE fois (extras retirés de l'intent après lecture ; bonus : tap notification app-ouverte ouvre aussi le dialog). Test UI `LogDoseRequestTest` + vérification émulateur. (c) **Rappel sauté si dose déjà loggée** (demande) : garde au déclenchement — `hasDoseLoggedOnDay` (pur, jour calendaire LOCAL) dans ReminderReceiver → notif sautée + re-programmation créneau suivant dans tous les cas ; 6 tests JVM + vérification A/B émulateur (EEn dose-du-jour → 0 notif, Fluoxetine sans dose → 1 notif). **259 tests JVM + 14 UI + lint** ; **web 180 tests + E2E verts** (check.sh). **Publication** : commit a4f859f + tag v1.10.0 poussés sur les 2 Gitea, APK release (2,7 Mo) + debug (21 Mo) publiés et vérifiés par téléchargement (cloudyfy id 77, farewell id 1588583). **Web miroir v1.10.0 sync** (fix #68 lab-timing, WEB_VERSION alignée, commit b865b16 + tag poussés ; #69/garde rappel = Android-only, notés dans le changelog web). |
|
||
| 22 sept. 2026 (session v1.9.8) | **Socle qualité : tests UI Compose + signature release dédiée + découpage Settings** (revue de code du 21 sept. : « pas de CI, pas de tests UI, APK signé clé debug, écrans qui grandissent »). (a) **13 tests UI instrumentés** (`app/src/androidTest/uitest/`, §8.ter) : navigation 5 onglets (preuves = éléments inconditionnels : chips 24 h, FAB), dialog Nouveautés (épinglage du fix v1.3.2 : ne réapparaît pas après dismiss+restart), sections Paramètres + version installée = BuildConfig (le bug #46 ne peut plus revenir) ; déterminisme = pm grant shell pré-launch + markChangelogSeen + assertions via targetContext (indépendant FR/EN) + zéro dépendance aux données Room ; deps androidTest (BOM + ui-test-junit4 + ext:junit-ktx + junit explicite). (b) **Signature release conditionnelle** (§16.quater) : `scripts/make-release-keystore.sh` (shellcheck+shfmt) génère keystore + keystore.properties (gitignorés), `assembleRelease` l'utilise s'il existe, sinon fallback clé debug inchangé ; migration backup→désinstall→réimport documentée. (c) **SettingsScreen découpé** (840 → 377 + 11 fichiers 33–164 lignes dans `ui/screens/settings/`, §10) — déplacement pur, commentaires historiques conservés, ChartScreen volontairement entier (garde de source #54). **Vérifications** : 249 tests JVM + 13 UI verts, lint vert, assembleDebug/Release OK, **smoke test RELEASE émulateur** (accueil, 5 onglets, Paramètres complet scroll compris — version 1.9.8 visible, éditeur traitement, dialog dose + garde « crée un traitement », 0 crash logcat — anti-#64). **README EN** (README.en.md, liens FR/EN croisés ; DEVELOPPEMENT.md reste FR par choix assumé). **Publication** : commit 09cddfc + tag v1.9.8 poussés sur les 2 Gitea, APK release (2,7 Mo) + debug (21 Mo) publiés et vérifiés par téléchargement (cloudyfy id 75, farewell id 1588581). Web miroir NON concerné (v1.9.8 = socle Android uniquement : tests instrumentés, signature APK, découpage Compose, README du dépôt Android — rien à porter côté web, qui reste à v1.9.7). |
|
||
| 20 sept. 2026 (session v1.9.7) | **Fix « la suggestion de prise de sang change tout le temps »** (remontée + données réelles : logs d'injection à heures variables) : la règle de stabilisation v1.8.1 comparait l'écart inter-doses EXACTEMENT à l'écart précédent — un log 30 min plus tard cassait le régime et repoussait la stabilisation de 5 × t½ à CHAQUE injection (creux fuyant). FIX : l'écart doit rester dans **l'intervalle de Posologie ± 24 h** (`GAP_TOLERANCE_MS`, fenêtre vs l'interval THÉORIQUE — des logs 6,8 j puis 7,2 j ne se déstabilisent plus en cascade ; un vrai passage à 2 j reste hors fenêtre). Le créneau affiché suit toujours la dernière dose réelle (voulu). Tests : « interval change » ré-épinglé + 2 tests de tolérance. 249 verts + lint. Web miroir v1.9.7 (12 verts lab-timing). §7.11 mis à jour. |
|
||
| 20 sept. 2026 (session v1.9.6) | **Carte Accueil « Prochaines doses » — toutes les prises, triées** (remontée : le rappel quotidien CPA masquait le rappel hebdomadaire EEn — « je ne le vois quasiment pas ») : la carte liste UNE ligne par traitement TRIÉES par prochaine prise (1ʳᵉ en avant, suivantes en style secondaire), titre pluriel dès 2 lignes (`next_doses` FR/EN) ; helper PUR `nextDoseLine` (locale/zone paramétrables, template jours localisé `next_dose_days`) — 3 tests `NextDoseLineTest`. Notifications inchangées (une alarme par traitement). Web miroir v1.9.6 : helper pur `nextDoseLineText` (format.js) + même carte + 3 tests miroirs (misc) — 247 verts Android / 178 web. |
|
||
| 20 sept. 2026 (session v1.9.5) | **Revue de code post-v1.9.4 — audit lecture-seule des deux dépôts puis corrections** (demande : « les logiques, choix techniques, commentaires, optimisations ») : (a) **#67** : ticks X alignés sur le fuseau APPAREIL pendant que les labels étaient formatés dans le fuseau CHOISI (zone manquante à `xLabelTicks`, contredisant #55) + pas fixe `+24 h` dérivant d'1 h aux transitions DST → zone passée + avancement Calendar, test DST (Europe/Paris 29/03) ; (b) `EstrannaiseCloud.compute` : `nowMs` mort supprimé (KDoc mensonger) ; (c) **perf** : `E2Context`/`prepareE2Context` — le tracé labs (720 pts) reconstruisait les maps moteur par point, diluant v1.8.2 (résultat bit-identique) ; Paints texte + `detectExtrema` + formatters mémoïsés dans le Canvas ; (d) **règles ré-appliquées** : `modelLabelRes` (anti-#59), `PKModels.*` partout (fini les literals), `scheduler2` supprimé ; (e) **doc rattrapée** : 3 commentaires périmés (ODS/lookup/t½-table), §7.2/§7.3 (asset historique, cutoff 10×t½). **Web miroir v1.9.5** : garde `hasModel` ESE dans `cutoffHours` (un backup importé ESE+ester non couvert levait TypeError → écran vide), portage des optimisations v1.8.2 (contexte préparé, cutoffs par traitement, Bateman paresseux), min/max en une passe (RangeError possible sur ~230 k pts avec le Nuage), init MCMC non bloquante (data-ready ⇒ asset chargé), `pk_profiles.json` exclu du zip (export-ignore, −548 Ko livrés), E2E version paramétrée (`data-version` — anti-bug v1.9.3), nettoyages (import mort lab-timing, JSDoc « troncature », dialogs ESE=3, COLORS accueil, JSDoc orpheline). 244 verts Android + lint · 175 verts web + E2E ✓. |
|
||
| 19 sept. 2026 (session v1.9.4) | **Presets du modèle ESE étendus à ses 6 esters injectables** (remontée : « depuis l'ajout du modèle source ESE, les presets n'ont pas été mis à jour ») : 3 presets ajoutés (EC huile 5 mg IM, EB 1 mg IM, EUCS suspension cristalline 10 mg SC — 25 entrées), champs Bateman informatifs = t½ des fits ; **éditeur** : la liste des esters suit `Esters.choicesForModel` (parité DoseDialog/web) et rebascule sur EV si l'ester sort de la couverture au changement de modèle ; **web** : 2 trous du portage v1.9.0 corrigés (`Esters.EUCS` manquant dans pk-engine.js — preset référençant `undefined` — et `choicesForModel` resté sur la couverture ODS à 3 esters) + `tests/presets.test.js` (6 tests, miroir `PKPresetsTest`). 243 verts / 213 sans données locales + lint + check web (175). Doc : doublon §7.12 supprimé. Web v1.9.4 sync. |
|
||
| 19 sept. 2026 (session v1.9.2) | **Fix nuage vide sans traitement stocké ESE** (remontée : « il ne s'active que autour du tracé émulé, pas autour du modèle Estrannaise ») : le filtre du nuage testait le pkModel STOCKÉ — or la courbe ESE redessine TOUTES les doses E2 (modelOverride ESE) quel que soit le modèle stocké. FIX : le nuage couvre les doses à ester effectif couvert par ESE (peu importe le stocké) ; l'exclusivité reste portée par le chip ; l'oral Bateman reste hors nuage. 2 tests réécrits (237 verts / 207 sans). Web miroir v1.9.2. |
|
||
| 19 sept. 2026 (session v1.9.1) | **Fix nuage calibré + suggestion sur l'Accueil + explication +4 pg/mL** : (a) remontée « le nuage ne s'active que autour du tracé, pas autour du modèle Estrannaise » : la courbe ESE est CALIBRÉE (scalePerEster) mais le nuage était BRUT → échelles différentes. FIX : EstrannaiseCloud.compute reçoit le MÊME scalePerEster que la courbe ESE (Android + web miroir) ; test nuage calibré ×2 (Android). (b) Le +4 pg/mL constaté depuis v1.8.2 : l'ARRONDI de la médiane (fix v1.8.2, plus juste que la troncature) a déplacé une échelle auto-calibrée d'un cran — le niveau actuel est le CORRIGÉ, documenté CHANGELOG 1.9.1. (c) Émulateur : nuage visible autour de la courbe ESE calibrée (1363 px rose pâle, seed réel + traitement EV ESE de test), 0 crash. Web v1.9.1 sync (harmonisation de l'arrondi — les niveaux web/android convergent). 235 verts / 205 sans données locales. |
|
||
| 17 sept. 2026 (session v1.9.0) | **Modèle Estrannaise ANALYTIQUE (abandon de l'ODS) + nuage d'incertitude MCMC (Android + web sync)** (demande) : découverte des sources originelles (github.com/WHSAH/estrannaise.js — forme close 3C + paramètres publiés + posterior MCMC 313/ester). Vérification numérique AVANT débranchement : **RMS 0,00 vs tables ODS** (l'ODS était l'échantillonnage de ces formules). ESE → analytique (dispatch, cutoff 10×t½, t½ LabTiming), PKProfileStore retiré du runtime (asset ODS → src/test/assets, −550 Ko d'APK), ESE couvre 6 esters (+EUCS), TFS = liste explicite (EUCS sans V3C TFS). **Nuage MCMC exclusif ESE** : chip `Nuage` (off, activable si ESE affiché, coupure auto si ESE off), 32 courbes du posterior déterministes, alpha faible sous les courbes, hors échelles/labels/extrema. 11 nouveaux tests (234 verts / 204 sans données locales) + lint + check web (166). Validé émulateur §16.ter : courbe ESE analytique affichée, unités, chips Extend/Cloud, 0 crash. |
|
||
| 17 sept. 2026 (session v1.8.2) | **Maintenance — audit complet code + doc** : (a) **perf moteur** : cutoffHours pré-calculé par traitement dans e2At/computeCurve (le chemin chaud du tracé labs recalculait ~430 k appels/refresh), Bateman paresseux (inutile pour les traitements à profil), doses pré-groupées par traitement — résultat identique (ordre de sommation préservé, 223 tests verts inchangés) ; (b) **2 fixes de fraîcheur** (#65 accueil : clés produceState sans allTreatments/tConfig — éditer un scaleFactor n'actualisait pas la courbe avant le tick ; #66 graphiques : clé `labResults.size` — éditer la valeur d'un lab ne recalculait pas) ; (c) **nettoyage** : ~9 imports morts, qualifications raccourcies, code mort (Repository/DAO, currentLevel, scheduler2), médiane factorisée (4 copies), SimpleDateFormat hors boucle, AppLog sans recopie, Regex précompilées, formatDose dédupliqué ; (d) **doc** : ~35 corrections (compteurs actuels 223/193 partout, TOC + ancres réparées, inventaire §8 à jour et fusionné, doublons CHANGELOG, §19 backup, fautes). Audit préalable par double exploration lecture-seule (46 fichiers + 4 docs). 223 verts + lint + émulateur smoke. Android seul (web non concerné — re-render complet, aucune release web). |
|
||
| 17 sept. 2026 (session v1.8.1) | **Fix stabilisation + suggestion sur l'Accueil** (critique v1.8.0 : « changé d'ester, de dosage ET de posologie, et l'app me disait stabilisée depuis février ») : le proxy « 1ʳᵉ dose du traitement » était aveugle aux changements récents. NOUVELLE règle `regimeStartMs` : le régime courant = la **séquence terminale de doses** à (ester effectif, dose mg, **écart inter-doses**) constants (l'écart comparé EXACTEMENT — 7 j ± 1 h casse le régime) ; tout changement récent réinitialise la stabilisation → la reco saute au premier creux post-stabilisation ; l'ester EFFECTIF de la dernière dose (override compris) alimente la carte. **+ Suggestion aussi sur l'Accueil** (demande) : carte compacte (creux daté + créneau, mention de stabilisation seulement si le régime n'était pas déjà stable). 3 nouveaux tests (223 verts / 193 sans données locales). Validé émulateur (release, seed réel) : la carte passe de « stabilisé depuis le 06/02 » à « EEN pas stabilisé avant le 29/09 — premier creux fiable », creux 27/09 22:43, 0 crash. Web v1.8.1 sync (3 tests miroirs + encart Accueil). |
|
||
| 17 sept. 2026 (session v1.8.0) | **Recommandation de prochaine prise de sang (page Analyses, Android + web sync)** (demande + choix validés) : carte « Prochaine prise de sang (suggestion) » — creux exact calculé sur la courbe prévisionnelle **juste avant l'injection suivante** (+ mention « ou la veille du créneau »), **saut au premier creux STABILISÉ** (5 × t½ terminale — pas de prise intermédiaire trompeuse), filtres honnêtes (injectable actif + Posologie requis, creux jamais déjà mesuré, horizon borné → carte cachée), **invite « renseigne une Posologie »** quand un injectable actif en est dépourvu (demande explicite). Moteur : `pk/LabTiming.kt` PUR (miroir web `js/pk/lab-timing.js`) ; `PKProfileStore.terminalHalfLifeDays` (refactor de la pente d'extrapolation de `sample()` — le modèle Estrannaise lit sa t½ dans la table, TFS/WHSAH analytiquement). UI : calcul `produceState` + tick minute, disclaimer, mutuellement exclusive avec l'invite. 11 nouveaux tests (220 verts / 190 sans données locales) + lint + check web (152). Validé émulateur §16.ter (release, seed réel v1.7.0) : « creux 20/09 10:37, injection EEN 20/09 11:20, EEN stabilisé depuis le 06/02 », 0 crash. |
|
||
| 16 sept. 2026 (session v1.7.1) | **Fix « une seule note sur deux » dans les Analyses (Android + web)** : une prise E2+T avec des notes DISTINCTES (note clinique E2 + note « DHT » sur T — cas réel) n'en affichait qu'UNE (l'autre conservée mais perdue à l'affichage). Cause : affichage « première note non vide du groupe » — hypothèse historique « toutes identiques » (dialog commun) cassée par l'édition unitaire. FIX : `labNotesForDisplay` PUR des deux côtés (notes distinctes → préfixées du marqueur ; identiques → dédupliquées ; vides ignorées) + UI branchée. + **Nouvel export réel v1.7.0** (3 traitements dont CPA oral, 64 doses, 28 labs, notes doses ET labs) → `local-test-data/backup-v1.7.0.json` (gitigné) + **régression n°5** data-driven (7 tests : parsing, double-note épingle le fix, plausibilité moteur avec CPA, prévision 7 j). 11 nouveaux tests (211 verts / 181 sans données locales) + lint + check web. Validé émulateur §16.ter : seed v1.7.0 → écran Analyses affiche « E2 : … » ET « T : … » (dump), 0 crash. Web v1.7.1 sync (+ leçon : oubli d'entrée CHANGELOG = dialog « Nouveautés » vide, attrapé par l'E2E/§7). |
|
||
| 13 sept. 2026 (session v1.7.0) | **Sauvegarde automatique quotidienne (opt-in) + unités des axes** : (a) Paramètres → « Sauvegarde automatique quotidienne » — dossier choisi UNE FOIS via SAF tree (ACTION_OPEN_DOCUMENT_TREE + takePersistableUriPermission, AUCUNE permission de stockage ; typiquement un dossier Owncloud synchronisé), WorkManager périodique 24 h (KEEP au démarrage, worker no-op si désactivé — pattern AlertWorker), **run immédiat à l'activation** (feedback + validation émulateur triviale), rétention configurable 1–30 copies (défaut 7) : fichier horodaté par run (`hormonetrack-auto-YYYYMMDD-HHmm.json`, jamais d'écrasement) + purge par la rétention PURE ([AutoBackupRetention] — ne touche JAMAIS exports manuels/logs/étrangers, keep clampé ≥ 1) ; statut « dernier run » persisté + AppLog complet ; contenu = backup v2 COMPLET (importable tel quel). (b) **Unités des axes du graphique** (remontée « jamais ajoutées depuis v1.0 ») : pg/mL (E2, gauche) / ng/mL (T, droite) au sommet des colonnes de labels (padTop 12→26 dp), portées aussi côté web (versions sync). 9 nouveaux tests (200 verts / 177 sans données locales) + lint vert ; proguard : `-keep` AutoBackupWorker (réflexion WorkManager — leçon #64). Validé émulateur §16.ter APK release (dossier Download via le picker SAF piloté uiautomator, fichier écrit, rétention, unités visibles, 0 crash). |
|
||
| 12 sept. 2026 (session v1.6.0) | **« Tracé labs » PROLONGÉ au-delà du dernier lab** (demande) : le tracé s'arrêtait AU dernier lab (v1.5.0) — justement la période la plus récente était invisible. FIX : chip `Prolonger` (5ᵉ de la rangée, off par défaut, désactivé tant que Tracé labs est off) → au-delà du dernier lab significatif, `courbe(t) = M(t) × ρ_last` (ρ CONSTANT — pas d'extrapolation de pente, elle divergerait sans base physiologique) ; M(t) inclut AUTOMATIQUEMENT les doses loguées après le dernier lab (une injection EV après des labs EEn refait monter la courbe — épinglé par test) ; horizon = dernière dose E2 + cutoffHours de son traitement (une seule source de vérité : `cutoffHours` rendu public) ; série SPLITTÉE en "LAB" (ancré) / "LABX" (prolongé, rose atténué α 0,55 + légende dédiée — l'estimation ne se confond pas avec le mesuré) ; branche "LABX" EXPLICITE dans le when de légende AVANT le else (leçon #63) ; **AVERTISSEMENT visible sous la légende quand la prolongation est affichée** (demande) : « simple simulation, sans garantie de correspondre au réel, basée sur tes labs qui peuvent eux-mêmes être erronés » (string FR/EN `lab_track_extend_warning`). < 2 ancres, modèle déjà éteint au dernier lab, ou demande finissant avant → pas de prolongation (retour v1.5.0 exact, épinglé bit-compatible). 8 nouveaux tests (191 verts / 168 sans données locales) + lint vert. **+ Rattrapage validation émulateur §16.ter (oubliée, demandée par l'utilisatrice) → bug #64 TROUVÉ** : « Tracé labs » vide EN RELEASE SEULE (R8 full mode avait REMOVÉ la classe — mapping `R8$$REMOVED$$CLASS` — et inliné le calcul dans le producer ; debug OK, 191 tests JVM aveugles). FIX : `-keep class ...LabTrajectoryModel { *; }` ; validé émulateur APK release re-buildé avec les données réelles seedées : ancrée (7090 px rose + légende), prolongée (légende + avertissement), 0 crash. Tag v1.6.0 reposé sur le commit de fix AVANT publication d'APK (checklist §16 étape 4). **Checklist §16 : étape 3.bis « validation émulateur release OBLIGATOIRE avant tag » ajoutée.** |
|
||
| 11 sept. 2026 (session v1.5.0) | **« Tracé labs »** (demande débattue) : courbe hybride `M(t)×ρ(t)` ancrée sur les labs — ρ log-linéaire entre labs, garde #61, E2 seul, fenêtre labs, chip off par défaut + scroll horizontal CONFINÉ (chip 4ᵉ coupé hors fenêtre, constat émulateur) + leçon #63 légende dupliquée ("LAB" tombant dans else TFS) ; ChartSeries.showExtrema=false pour la série ; 11 tests LabTrajectoryModelTest (183 verts) + lint ; validé émulateur (courbe passe exactement sur le lab 248, 0 crash). |
|
||
| 5 sept. 2026 (session 1) | Plan, vérification GT 3 = Lite Wearable, création couche données + ancien moteur Bateman + ancien ReminderManager. Extraction des modèles du `Estrogen.ods` → `/tmp/pk_models.json` (6 profils × 8001 h + params D/k1–k3). |
|
||
| 5 sept. 2026 | L'utilisatrice mentionne un travail d'un assistant tiers « **Mimo V2.5** » : **aucune trace trouvée** (fichiers identiques à la session 1, timestamps identiques). Reprise depuis l'état existant. Bugs trouvés au passage : settings.gradle, BootReceiver, cancel PendingIntent. |
|
||
| 5 sept. 2026 (session build) | Redesign données (ester/pkModel/scaleFactor), réécriture moteur PK sur tables ODS, modèle T + calibration, rappels complets, UI 6 écrans, chart Canvas, backup JSON, i18n, wrapper Gradle, guide. **Installation SDK Android (brew) + premier build.** |
|
||
| 5 sept. 2026 (session tests/docs) | Correction de toutes les erreurs de compilation (dont 3 vrais bugs logiciels trouvés par les tests), **24 tests unitaires verts**, APK debug généré (18 MB), documentation complète (README + docs/), préparation repo git. |
|
||
| 5 sept. 2026 (session v1.1.0) | Bugs remontés par l'utilisatrice : courbes vides (casse EEn) + pas d'édition des doses → corrigés ; régression épinglée sur ses données réelles ; APK v1.1.0. |
|
||
| 5 sept. 2026 (session v1.2.0) | **Repo git initialisé** (commits par couche + tags de release) ; montée toolchain AGP 9.4/Gradle 9.7.1/Kotlin 2.3.21/BOM 2026.08.00/compileSdk 37 ; panoramique du chart, superposition des deux modèles, prévision par « Fréquence », calibration automatique optionnelle, intervalles entre doses, TimePicker centré ; **36 tests verts**, APK v1.2.0 (23 MB). |
|
||
| 5 sept. 2026 (session builds release) | Question debug vs release → **builds release optimisés** (R8 full mode + shrinkResources, 20 Mo → **2,4 Mo**, signés avec la clé debug → upgradables sans perte) ; garde-fous Gson dans `proguard-rules.pro` (réflexion) ; releases publiées avec les **deux APK** (release recommandé + debug) ; 62 tests verts. |
|
||
| 5 sept. 2026 (session v1.2.5) | Bug graphique : double espace en haut (double insets) → **edge-to-edge propre** (`enableEdgeToEdge` + insets consommés une seule fois) ; 62 tests verts, APK v1.2.5 + release. |
|
||
| 5 sept. 2026 (session v1.2.4) | Bug `isActive` (simulation effacée) → sémantique « drapeau administratif » (§6.bis) ; régression n°3 sur le 3ᵉ export réel (transition EV inactif → EEn actif, 38 doses, 22 labs) ; **62 tests verts**, APK v1.2.4. |
|
||
| 5 sept. 2026 (session push Gitea + v1.2.3) | Push initial vers **gitea.cloudyfy.fr/Siphonight/HormoneTrack** (privé) après **anonymisation de l'historique** (filter-branch : les premiers commits embarquaient les valeurs réelles des tests) ; **données de test réelles déplacées hors dépôt** (`local-test-data/` gitignoré, tests `Assume`-skippés) ; **releases avec APK en pièce jointe** ; pics/creux + calibration T par période d'ester ; **56 tests verts**. |
|
||
| 5 sept. 2026 (session v1.2.1) | Remontées UX/utilisateur : calibration **par période d'ester** (labs EV → doses EV, labs EEn → doses EEn — corrige les courbes gonflées), lab T "pg/mL" neutralisé, **édition des labs**, tap accueil → Graphiques + mini-légende, auto-cal fusionnée (E2+T, un seul switch) ; 2ᵉ régression épinglée sur le nouvel export ; **44 tests verts**, APK v1.2.1. |
|
||
| 5 sept. 2026 (session v1.2.2) | Toggle T masque aussi les labs T ; **prise de sang E2 + T en une entrée** (chacune optionnelle) affichée **côte à côte** ; sélecteur d'édition par marqueur ; suppression par prise ; 4 tests de regroupement ; **48 tests verts**, APK v1.2.2. |
|
||
| 5 sept. 2026 (session v1.2.6) | Valeurs estimées affichées sur les pics/creux (drawExtremum) ; **import JSON en mode écrasement** (bug : conflit d'IDs si données existantes) avec restauration du tConfig et reprogrammation des rappels ; docs ; 62 tests verts, APK v1.2.6 + releases. |
|
||
| 6 sept. 2026 (session v1.3.1) | En-tête « temps sous THS » sur Doses ; **logs de diagnostic** (AppLog : buffer 500 lignes, persisté, exportable de Paramètres) ; **fix toggle agenda** (callback async) ; **87 tests verts**, APK v1.3.1 + releases. |
|
||
| 6 sept. 2026 (session v1.3.2) | 3 bugs remontés : toggle agenda (callback async fix + ContextCompat au save + Posologie requise), export logs plantait (pattern JSON réutilisé + AppLog), dialog changelog récurrent (version vue mémorisée avant affichage + titre BuildConfig documenté) ; export réel mis à jour (3 traitements : EV inactif + EEn actif + CPA oral) ; **87 tests verts**, APK v1.3.2 + releases. |
|
||
| 6 sept. 2026 (session farewell) | Repo créé côté farewell → **push SSH (alias `farewell` : giteassh:2222) + 12 releases publiées avec APK vérifiés par téléchargement** ; piège lexicographique `v1.2.10 < v1.2.5` en comparaison de chaînes épinglé (§14 #38) ; les deux instances Gitea sont synchrones. |
|
||
| 6 sept. 2026 (session v1.3.3) | **Audit de reprise de maintenance** (nouvelle session IA) : 3 bugs racines trouvés — permissions agenda ABSENTES du manifest (jamais déclarées, §14 #44), export logs plantait TOUJOURS (le fix v1.3.2 réimplémentait l'IO au lieu de réutiliser BackupManager, §14 #45), bump de version JAMAIS commité (tags v1.3.0–1.3.2 tous `versionCode 14 / "1.3.0"`, §14 #46). Corrigés + doc rafraîchie (DB v3, targetSdk 36, régression 3 = export v1.3.1, §19/§20 staleness) ; **téléphone de test documenté : Google Pixel 9 /e/OS** (AOSP ; SAF DocumentsUI standard). |
|
||
| 6 sept. 2026 (session v1.3.4) | **Deux crashs v1.3.3 remontés (Doses + export logs)** → reproduits sur ÉMULATEUR avec les vraies données (recette §16.ter, désormais standard) : (a) Doses = string `hrt_duration` 3 placeholders vs 2 args (**né en v1.3.1**, §14 #47) ; (b) export logs = `LocalDate.format("…-HHmm")` levant `UnsupportedTemporalTypeException` au TAP (§14 #48) — les fixes IO v1.3.2/v1.3.3 étaient à côté du vrai problème. Lint mis en filet bloquant (aurait attrapé les deux) + #49 notif ; helper `ExportFileNames` + 3 tests (90 verts) ; fixes VÉRIFIÉS sur émulateur avec l'APK release + données réelles (flux SAF complet : SAVE → fichier + message). Publication v1.3.4. |
|
||
| 6 sept. 2026 (session v1.3.5) | **Premier diagnostic à distance RÉUSSI via les logs exportés** : les AppLog de la v1.3.4 prouvent export logs `ok=true` (fix #48 ✓ au téléphone), permission agenda accordée (fix manifest ✓) et révèlent le bug racine EXPLICITE : `ensureCalendar` plantait car l'URI sync-adapter n'embarquait pas ACCOUNT_NAME/ACCOUNT_TYPE (§14 #50). En validant sur émulateur, DEUX bugs découverts : delete d'event sans account (#50 bis — « supprimé » logué mais l'event restait) et `buildTreatment` sans `calendarEventId` (#51 — événements orphelins). Cycle ON→save→OFF→save validé end-to-end (`content query`). Publication v1.3.5. |
|
||
| 6 sept. 2026 (session v1.4.0) | **Modèle TFS reconstruit sur la méta-analyse officielle** (params V3C du simulateur transfemscience.github.io/injectable-e2-simulator, extraits de `ester-data.js`/`calc-curve.js`) : forme close 3 compartiments, 7 esters (EB/EC/ECS/PEP ajoutés), pics de l'article reproduits à ~1 % (AUC + Figure 11 épinglés). **+ Fix #52** (remontée) : rappels quotidiens même hors jour d'injection → grille Posologie (moteur = source unique), re-programmation après notif et « Pris ». 12 nouveaux tests (102 verts), lint vert. Publication v1.4.0. |
|
||
| 6 sept. 2026 (session v1.4.1) | **UX graphique et carte « Prochaine dose »** (remontées) : (a) activer la Prévision ne saute PLUS dans le futur — la fenêtre reste en place, la projection s'étend au-delà et se parcourt en tirant vers la gauche (panHours NÉGATIF = futur, clamp unique `clampPanHours`, horizon 12× Posologie borné 30 j–1 an) ; (b) delta en JOURS sur la carte « Prochaine dose » au-delà de 24 h (`HrtDuration.daysAndHours` + `next_dose_days` FR/EN). 5 tests (112 verts). Publication v1.4.1. |
|
||
| 6 sept. 2026 (session v1.4.2) | **Seuils d'alerte configurables + notification** (demande) : limites E2/T hautes/basses (Paramètres, opt-in, validées haut > bas), cartes d'avertissement sur l'accueil (niveau ESTIMÉ actuel), **notification** via un worker WorkManager périodique 15 min (enfin utilisé !) + check one-time au save, anti-spam par état persisté (`Alerts.encodeState`/`shouldNotify`), canal dédié `hormonetrack_alerts`. + **backup JSON v2** : les PARAMÈTRES utilisateur (langue, auto-cal, seuils) voyagent dans l'export et sont restaurés à l'import (rétrocompat v1, `-keep` R8 pour UserSettings). 9 tests (129 verts). Publication v1.4.2. |
|
||
| 7 sept. 2026 (session v1.4.3) | **Question pharmacocinétique → 1 bug + 1 confirmation** : « l'estimation ne remonte pas le lendemain de mon injection EEn » → (a) **comportement NORMAL** (contribution J+1 < 12 % du total, plateau d'équilibre — épinglé par la régression n°4 sur le 4ᵉ export réel) ; (b) **bug #53** : le delta « vs il y a 6 h » de l'accueil comparait en réalité il y a 24 h (`firstOrNull` sur la fenêtre 24 h) → helper `pointHoursBefore` + 4 tests. 137 verts. Publication v1.4.3. |
|
||
| 7 sept. 2026 (session v1.4.4) | **Prévision inaccessible depuis v1.4.1** (#54, remontée) : DEUX causes emboîtées — (a) captures FIGÉES dans `pointerInput(Unit)` (`futurePanHorizon` lu à 0 pour toujours → pan futur interdit) → `rememberUpdatedState` + **garde de source** (`ChartScreenSourceGuardTest`, le lint Compose ne voit pas ce pattern) ; (b) toggle sans effet visible (`endMs = now` → 0 dose générée) → extension contrôlée à droite (`forecastExtensionHours`, jusqu'au 1ᵉʳ créneau, sans déplacer le début). Validé émulateur : drag gauche → panHours < 0, courbe dessinée au-delà de « maintenant » (screencap). 140 verts. Publication v1.4.4. |
|
||
| 7 sept. 2026 (session v1.4.5) | **« La prévision simule le 13 au lieu du 12 »** (#55) : diagnostic sur données réelles — GÉNÉRATION EXACTE (créneau = dernière dose loguée + 6 j ; subtilité : la dose avait été loguée le LENDENDE de l'injection réelle → dérive d'un jour par cycle, l'app suit les logs) ; illusion causée par l'absence de marqueur + labels X à minuit UTC. FIX : marqueurs de doses (prévisionnels pointillés + réels discrets), labels à minuit LOCAL, fuseau de graphique configurable (Paramètres). **+ #56** : boucle de recomposition (`nowMs` relu à chaque frame → endMs dérive → saturation main thread → taps perdus) → mémoïsé sur le tick. 145 verts. Publication v1.4.5. |
|
||
| 7 sept. 2026 (session v1.4.6) | **3ᵉ modèle PK « WHSAH »** (demande) : le fit « license-free » du WHSAH Collective publié dans **Mona** (l'app concurrente — son git montre qu'elle utilisait NOS paramètres TFS avant mars 2026) intégré au même niveau : dispatch parallèle (aucun remplacement), 6 presets, dropdown 3 choix, 3ᵉ toggle superposable (vert/violet), PEP non couvert. Comparaison chiffrée TFS vs WHSAH épinglée (EV pic 367 vs 295 ; EEn à J+1 ×3). **+ #57** : l'écran Graphiques défile désormais (rangée de toggles coupée sous le pli, trouvé en émulateur). 155 verts. Publication v1.4.6. |
|
||
| 7 sept. 2026 (session v1.4.7) | **Toggles alignés sur les traitements** (#58, demande) : au chargement, seuls les modèles utilisés par des traitements à PROFIL PK (`usesProfileModel`) sont ON (l'oral/AA polluait le set — vu en émulateur avec le CPA) ; garde-fou TFS si aucun profil ; une seule init. **Épinglé : la calibration s'applique à TOUTES les courbes** (par ESTER via doseEster, indépendant du modelOverride — test). 158 verts. Publication v1.4.7. | + **#59** : le chip de la carte de traitement affichait « Estrannaise » pour WHSAH (ternaire à 2 branches → `modelLabelRes` centralisé).
|
||
| 7 sept. 2026 (session v1.4.8) | **Calibration PAR MODÈLE PK** (#60, remontée : « ×2,21 sur WHSAH alors que l'idéal serait < 1 ») : un seul jeu de facteurs (calculé depuis le modèle stocké) était partagé par toutes les courbes → absurde dès qu'un traitement basculait de modèle. FIX : le graphique calcule une AutoCalibrated PAR modèle affiché (ESE/TFS/WHS — modelOverride propagé à computeEsterScaleFactors et computeTKPerEster) → chaque courbe calibrée passe par les labs. Home inchangé (modèle stocké). Test : chaque courbe calibrée ≈ lab (±2 %), échelles TFS ≠ WHS. 159 verts. Publication v1.4.8. |
|
||
| 7 sept. 2026 (session v1.4.9) | **Fix #61 — calibration tombant à ×2,21 (auto ET manuelle)** : reproduction par test sur données réelles — une dose EEn de test de JANVIER (10 mg) dans l'export, des labs de janvier-mars à 15–76 j → prédiction WHSAH résiduelle (184 → 1) → ratios aberrants (×3,3 → ×391) → médiane ×2,21, dans les DEUX pipelines. FIX : garde de significativité `labIsSignificant` (prédiction ≥ 15 % du max observé, labs par timestamp croissant) sur les 3 pipelines ; facteur WHSAH retombe à 0,55. + Hint UX (auto ON → facteur manuel ignoré à l'affichage) + nettoyage CHANGELOG (4 titres orphelins doublés [1.4.4–1.4.7]). 168 verts. Publication v1.4.9. |
|
||
| 7 sept. 2026 (session v1.4.10) | **Pan mort sur la vue 24 h** (#62, remontée par test utilisateur Pixel 9 : « le glisser horizontal ne marche pas sur 24 h, uniquement au-dessus ») : les deltas de doigt (~30 px ≈ 0,67 h à 24 h) étaient tronqués à 0 par le cast entier à CHAQUE événement de mouvement → le pan ne bougeait jamais sur 24 h. FIX : cumul fractionnaire (`panDeltaHours` — pur, testé : 2 deltas de 0,67 h → 1 h). **Subtilité de test documentée (§16.ter)** : l'émulateur headless ne délivre pas les mouvements intermédiaires au chart via `adb input swipe` → le pan n'est PAS testable par adb (l'utilisateur a validé sur Pixel 9). 172 verts. Publication v1.4.10. |
|
||
| 6 sept. 2026 (session v1.3.0) | Dialog « Nouveautés » après mise à jour (CHANGELOG embarqué en asset, tâche `copyChangelog`, version vue en DataStore) ; **événements d'agenda récurrents** (calendrier local HormoneTrack, RRULE posologie, permission runtime, Room v3 `calendarEventId`) ; Paramètres : version + lien releases ; **78 tests verts**, APK v1.3.0 + releases. |
|
||
| 5 sept. 2026 (session v1.2.10) | Sens des boutons de zoom inversé (+ = zoom avant, convention carte — retour utilisateur) ; 67 tests verts, APK v1.2.10 + releases. |
|
||
| 5 sept. 2026 (session v1.2.9) | **Zoom du graphique** (pinch + boutons, 6 h → 300 j, focal stable, échantillonnage adaptatif `stepForRange`, labels X 1 h/3 h) ; **README : disclaimer IA-assisté** en en-tête ; 3 tests ; **67 tests verts**, APK v1.2.9 + releases. |
|
||
| 5 sept. 2026 (session v1.2.8) | « Fréquence d'injection » → **« Posologie »** (terme adapté à toutes les voies) ; quirk Gitea découvert (noms d'assets normalisés à l'upload) → renommage PATCH dans le script ; releases avec les 2 APK ; 64 tests verts. |
|
||
| 5 sept. 2026 (session v1.2.7) | Prévision : les créneaux passés ne sont plus simulés (bug oubli → faux pic historique) ; retard = décalage voulu ; mismatch doc-code §7.3b corrigé ; 2 tests ; **64 tests verts**, APK v1.2.7 + releases. |
|
||
|
||
Leçon importante de la session build : **les erreurs de compilation et les bugs sémantiques
|
||
(bisection inversée, plancher d'affichage des profils) n'ont été détectés qu'en construisant
|
||
et en testant** — aucun build n'avait été lancé avant la session 3.
|
||
|
||
## 3. Stack & versions (épinglées — à jour v1.10.0)
|
||
|
||
| Composant | Version | Où |
|
||
|---|---|---|
|
||
| Gradle | **9.7.1** (wrapper) | `gradle/wrapper/gradle-wrapper.properties` |
|
||
| AGP | **9.4.0** — **Kotlin intégré** : ne PAS appliquer `org.jetbrains.kotlin.android` ; `kotlinOptions` supprimé (cible JVM via `compileOptions`, 17) | `build.gradle.kts` racine |
|
||
| Kotlin | 2.3.21 (plugin compose 2.3.21) | idem |
|
||
| KSP | 2.3.11 (versionnage indépendant depuis KSP2) | idem |
|
||
| Compose BOM | **2026.08.00** (Compose 1.12 ; material3 pinné par le BOM) | `app/build.gradle.kts` |
|
||
| Room | 2.8.4 (KSP) — **DB v3 + MIGRATION_1_2 (forecastIntervalDays) + MIGRATION_2_3 (calendarEventId)**, `fallbackToDestructiveMigration` retiré | idem + `AppDatabase.kt` |
|
||
| Navigation Compose | 2.10.0 | idem |
|
||
| AppCompat | 1.8.0 (langue par app) | idem |
|
||
| DataStore Preferences | 1.2.1 | idem |
|
||
| Gson | 2.14.0 | idem |
|
||
| JUnit | 4.13.2 (testImplementation + androidTestImplementation explicite) | idem |
|
||
| Tests UI Compose | **ui-test-junit4 via le BOM** + `androidx.test.ext:junit-ktx:1.3.0` + `androidx.test.uiautomator:uiautomator:2.3.0` (androidTest, v1.9.8/v1.10.0 — §8.ter) | idem |
|
||
| WorkManager | 2.11.2 (**UTILISÉ depuis v1.4.2** : worker périodique des seuils d'alerte, cf §9.bis) | idem |
|
||
| compileSdk / targetSdk | **37** / **36** ; minSdk 26 ; Java target 17 | app |
|
||
| buildFeatures | compose + **buildConfig** (VERSION_NAME pour l'app) | app |
|
||
|
||
Notes importantes (v1.2.0) :
|
||
- **AGP 9** : Kotlin est intégré à AGP — appliquer `org.jetbrains.kotlin.android` est une
|
||
erreur ; le plugin `org.jetbrains.kotlin.plugin.compose` reste appliqué normalement.
|
||
- **compileSdk 37** : la plateforme `platforms;android-37` n'était pas dans sdkmanager
|
||
(API 37 en preview à la date du build) mais **AGP l'a auto-téléchargée** (licences
|
||
signées) — le build passe.
|
||
- **Compose 1.12 (BOM 2026.08.00) exige compileSdk ≥ 37 et AGP ≥ 9.1** ; le BOM
|
||
2026.06.01 est le dernier compatible compileSdk 36.
|
||
- **Material Expressive** : `MaterialExpressiveTheme` / `ExperimentalMaterial3ExpressiveApi`
|
||
sont encore **internal** dans la ligne material3 pinnée par ce BOM (erreur de
|
||
compilation vérifiée — javap montre `public` JVM mais la visibilité **Kotlin** est
|
||
internal). `MaterialTheme` standard conservé ; basculer dès que l'API devient
|
||
publique (NOTE dans `ui/theme/Theme.kt`).
|
||
- Kotlin 2.0 → compose compiler via `org.jetbrains.kotlin.plugin.compose`. Room convertit
|
||
les enums ↔ String automatiquement. **Ne pas monter Kotlin/AGP/Gradle sans vérifier la
|
||
matrice de compatibilité** (les versions sont récupérées via maven-metadata.xml de
|
||
dl.google.com / repo1.maven.org, pas devinées).
|
||
|
||
## 4. Environnement de build (cette machine)
|
||
|
||
- **macOS (Apple Silicon), brew présent, Java 21 (Microsoft OpenJDK) sur `/usr/bin/java`** ✓
|
||
- **Téléphone de test** : **Google Pixel 9 sous /e/OS** (ROM dé-Googlée, base AOSP) —
|
||
SAF/DocumentsUI standard (l'export JSON via `CreateDocument` y fonctionne → c'est LE
|
||
pattern d'IO de référence pour tout export de fichier) ; pas de Play Store, sideload
|
||
par `adb install` ou APK direct. Les retours utilisateur (bugs, exports JSON) viennent
|
||
de ce téléphone. (Historique : la montre est une Huawei Watch GT 3 via Gadgetbridge,
|
||
cf §17 — ce n'est PAS le téléphone.)
|
||
- **Émulateur local (installé en v1.3.4)** : SDK brew =
|
||
`/opt/homebrew/share/android-commandlinetools`. Packages installés :
|
||
`emulator` 37.1.11 + `system-images;android-31;aosp_atd;arm64-v8a` (ATD : léger,
|
||
boot rapide, MAIS DocumentsUI STUBBÉ (`fakesystemapp`) → SAF non pilotable) et
|
||
`system-images;android-36;google_apis;arm64-v8a` (image complète — REQUISE pour
|
||
tester les flux SAF). AVD `hrt` (ATD 31) et `hrt36` (API 36) créés via
|
||
avdmanager (erreur béninie « devices.xml » au create : l'AVD est créé quand même).
|
||
Boot headless : `emulator -avd hrt36 -no-window -no-audio -no-boot-anim -no-snapshot
|
||
-gpu swiftshader_indirect`. Recette de test → **§16.ter**.
|
||
- **SDK Android** : installé via `brew install --cask android-commandlinetools`
|
||
→ `/opt/homebrew/share/android-commandlinetools` (524 MB)
|
||
- licences acceptées : `yes | sdkmanager --licenses`
|
||
- paquets (état actuel, cf §18) : `platform-tools`, `platforms;android-34/36/37`, `build-tools;34.0.0` etc.
|
||
- **`local.properties`** à la racine (non commité) : `sdk.dir=/opt/homebrew/share/android-commandlinetools`
|
||
- Gradle 9.7.1 téléchargé par le wrapper ; caches `~/.gradle` ≈ 1,5 GB
|
||
- Build initial validé (5 sept. 2026) : `./gradlew assembleDebug testDebugUnitTest` →
|
||
**BUILD SUCCESSFUL** ; l'APK debug actuel pèse ≈ 20 Mo (`app/build/outputs/apk/debug/app-debug.apk`)
|
||
|
||
## 5. Architecture générale
|
||
|
||
Pas de ViewModel ni de DI externe — volontairement simple pour une v1 :
|
||
|
||
```
|
||
HormoneTrackApp (Application)
|
||
└─ AppContainer
|
||
├─ AppDatabase (Room singleton)
|
||
├─ HormoneRepository (DAOs : Flow réactifs + one-shots suspend)
|
||
└─ AppSettings (DataStore : TConfig, langue)
|
||
|
||
MainActivity (AppCompatActivity)
|
||
└─ setContent { HormoneTrackTheme { HormoneTrackRoot } }
|
||
├─ CompositionLocal LocalAppContainer
|
||
└─ NavHost + NavigationBar (5 tabs + settings + treatment_edit/{id})
|
||
|
||
Écrans = collectAsState sur les Flows + calcul PK dans produceState(Dispatchers.Default)
|
||
```
|
||
|
||
Points clés :
|
||
- `HormoneTrackApp.onCreate()` : init `EstrannaiseModels` (asset MCMC —
|
||
`PKProfileStore` HORS runtime depuis v1.9.0), canal de notification
|
||
- `MainActivity` : applique la langue sauvegardée (`AppCompatDelegate.setApplicationLocales`),
|
||
demande POST_NOTIFICATIONS (API 33+) ; demande « logger une dose »
|
||
(`open_log_dose` + `treatment_id` venus de la notification) = `MutableState<Long?>`
|
||
détenu par l'activité, **consommé UNE fois** par Home via
|
||
[HormoneTrackRoot] (fix #69 — extras retirés de l'intent après lecture,
|
||
tap notification app-ouverte géré par `onNewIntent`)
|
||
- **Tout calcul PK est hors UI thread** (`produceState` + `Dispatchers.Default`)
|
||
|
||
## 6. Modèle de données (Room)
|
||
|
||
DB `hormonetrack.db`, **version 3**, migrations explicites (⚠️ plus de
|
||
`fallbackToDestructiveMigration` — retiré en v1.2.0 car l'utilisatrice a des données
|
||
réelles ; toute évolution de schéma = `Migration(x, y)` + ALTER TABLE) :
|
||
|
||
- **v1 → v2** : `forecastIntervalDays REAL` (prévision, v1.2.0)
|
||
- **v2 → v3** : `calendarEventId INTEGER` (agenda récurrent, v1.3.0)
|
||
|
||
### `Treatment` (treatments)
|
||
- base : `id`, `name`, `type` (ESTRADIOL/ANTI_ANDROGEN/PROGESTOGEN/OTHER), `route`
|
||
(ORAL/TRANSDERMAL_GEL/TRANSDERMAL_PATCH/INJECTION_IM/INJECTION_SUBCUT/OTHER),
|
||
`doseAmount`, `doseUnit`, `isActive`, `notes`, `createdAt`
|
||
- PK par table : `esterType` ("NONE"/"EV"/"EU"/"EEN" — objets `Esters`), `pkModel`
|
||
("ESE"/"TFS" — objets `PKModels`)
|
||
- PK Bateman : `absorptionHours` (Tmax), `eliminationHalfLifeHours`, `bioavailabilityFraction`
|
||
- Calibration : `scaleFactor` (défaut 1.0)
|
||
- **Prévision (v1.2)** : `forecastIntervalDays: Double?` (jours ; null = pas de
|
||
simulation à venir) — colonne ajoutée par la **migration Room v1→v2**
|
||
- Rappel : `reminderHour/Minute/Enabled`
|
||
- Helpers : `isInjection` (IM/SC), `usesProfileModel` (injection **et** ester ≠ NONE)
|
||
|
||
### `DoseLog` (dose_logs)
|
||
FK → treatments (CASCADE), index `treatmentId` + `timestamp`. `esterType: String?` =
|
||
**override par injection** (l'ODS permet de switcher d'ester d'une injection à l'autre) ;
|
||
null = ester du traitement.
|
||
|
||
### `LabResult` (lab_results)
|
||
`marker` libre ("E2", "T", "PRL"…), `value`, `unit` libre. La calibration et les charts
|
||
comparent `marker.equals("E2", true)` / `"T"` — **les dropdown suggèrent E2/T** ; si
|
||
l'utilisatrice tape autre chose, la calibration ignorera ces labs.
|
||
|
||
### DAOs
|
||
`Flow` pour l'UI + one-shots `suspend *Once()` pour backup/boot/calibration :
|
||
`TreatmentDao.getActiveOnce/getAllOnce`, `DoseLogDao.getAllOnce`, `LabResultDao.getAllOnce`.
|
||
|
||
### 6.bis Sémantique `isActive` (v1.2.4) — drapeau administratif, JAMAIS un filtre
|
||
|
||
`Treatment.isActive = false` signifie « ce traitement ne produit plus de nouvelles
|
||
doses ». **Ce n'est PAS un filtre de données** — passer un traitement à inactif ne
|
||
doit rien effacer (bug v1.2.4 : Home/Chart collectaient `activeTreatments`, la
|
||
simulation de tout l'historique EV disparaissait) :
|
||
|
||
| Effet de l'inactivation | Où |
|
||
|---|---|
|
||
| ✅ Retiré des chips « Log rapide » (accueil) | `HomeScreen` → `activeTreatments` |
|
||
| ✅ Retiré du dropdown des **nouvelles** doses | `DosesScreen` : création → actifs only ; **édition → tous** (une dose existante reste rattachée à son traitement inactif) |
|
||
| ✅ Rappels annulés (au save, au boot, au calcul de « prochaine dose ») | `rescheduleAll` + `nextReminderFireMs` (filtre `isActive`) ; `BootReceiver` lit `getAllOnce()` pour annuler les alarmes des inactifs |
|
||
| ✅ **Plus AUCUN créneau prévisionnel (v1.12.0, fix #70)** | `generateForecastDoses` garde `!isActive` — ni graphique Prévision, ni extension de fenêtre, ni horizon LabTiming ; le chip Prévision du graphique ne considère que les Posologies ACTIVES |
|
||
| ❌ JAMAIS retiré de la simulation | Home/Chart passent **`allTreatments`** au moteur — les écrans ne filtrent jamais |
|
||
| ❌ JAMAIS retiré de la calibration (auto) | `autoCalibrated(allTreatments, …)` |
|
||
| ❌ JAMAIS supprimé de l'historique des doses | `DosesScreen` liste tout, suppression manuelle uniquement |
|
||
|
||
Épinglé par la régression n°3 (`RegressionUserCase3Test`, cf §8) : EV inactif + EEn
|
||
actif en même temps, les 22 labs couvrant les deux périodes doivent rester simulés
|
||
et calibrés.
|
||
|
||
## 7. Moteur pharmacocinétique
|
||
|
||
`pk/PharmacokineticEngine.kt` + `pk/PKProfileStore.kt`.
|
||
|
||
### 7.1 Source : `Estrogen.ods`
|
||
|
||
- Fichier : le tableur `Estrogen.ods` de l'utilisatrice (Owncloud, 30 MB, 13 tables)
|
||
- 6 tables nominatives (surnoms anonymisés) : historique injections (datetime, cuisse
|
||
L/R, ester, dose mg, Z-track) + labs (E2 pg/mL, T ng/mL) + **facteur d'échelle**
|
||
manuel par profil (valeurs entre 0,5 et 1,4)
|
||
- Table **« Models »** : paramètres D, k1, k2, k3 par ester×modèle + **profils horaires
|
||
normalisés (pg/mL par mg) sur 8001 h** — ce sont ces tables qui sont consommées
|
||
- Les profils affichent 2 décimales → **plancher 0,01 / 0,00** en queue (conséquence
|
||
importante, cf §7.2)
|
||
|
||
Pics de référence (pg/mL par mg) :
|
||
|
||
| Clé | Modèle | Ester | Pic | Tmax |
|
||
|---|---|---|---|---|
|
||
| `EV_ese` | Estrannaise | valerate | 61,12 | ~45 h |
|
||
| `EU_ese` | Estrannaise | undecylate | 3,44 | ~55 h (plateau très long) |
|
||
| `EEn_ese` | Estrannaise | enanthate | 31,35 | ~152 h |
|
||
| `EV_tfs` | Transfem Science | valerate | 58,96 | ~51 h |
|
||
| `EU_tfs` | Transfem Science | undecylate | 10,11 | ~198 h |
|
||
| `EEn_tfs` | Transfem Science | enanthate | 31,97 | ~156 h |
|
||
|
||
### 7.2 `PKProfileStore` (⚠️ HORS RUNTIME depuis v1.9.0 — voir §7.12)
|
||
|
||
- **v1.9.0 (§7.12)** : le moteur Estrannaise est devenu ANALYTIQUE (forme
|
||
close de estrannaise.js) et `PKProfileStore` a quitté le runtime (asset
|
||
déplacé en `src/test/assets/`, −550 Ko d'APK). Ce qui suit décrit
|
||
l'architecture HISTORIQUE, encore active pour le TEST de fidélité RMS.
|
||
- Asset `src/test/assets/pk_profiles.json` : `{ "params": {D/k1/k2/k3…},
|
||
"profiles": { "EV_ese": [8001 floats], … } }` (550 KB, parse ~ms via `JsonParser`)
|
||
- **`initWithJson(json)`** = point d'entrée testable (JVM) ; `init(context)`
|
||
(ancienne lecture d'asset) n'existe plus au runtime
|
||
- `sample(ester, model, dtHours)` :
|
||
- modèle **strict** : seul "TFS"→`tfs` et "ESE"→`ese` ; tout autre → 0 (piège corrigé,
|
||
cf §14)
|
||
- interpolation **linéaire** entre heures entières
|
||
- **extrapolation terminale** : dernier point **≥ 1 % du pic** (pour éviter le plancher
|
||
d'affichage 0,01/0,00 de l'ODS), pente = décroissance moyenne sur les 48 h précédentes
|
||
(jamais avant le pic)
|
||
- ⚠️ tous les calculs en **Double** (Float×Double n'existe pas en Kotlin — source d'erreurs
|
||
de compilation, cf §14)
|
||
|
||
### 7.3 Superposition
|
||
|
||
Contribution d'une dose = `sample(...) × dose_mg` ; niveau total = somme des contributions
|
||
de toutes les doses E2, chacune multipliée par le `scaleFactor` de son traitement.
|
||
Coupure par dose `cutoffHours` (§7.12 pour l'ESE actuel) : **10 × t½ terminale**
|
||
du modèle de profil (v1.9.0 — analytique), `30 × t½` pour Bateman ;
|
||
la longueur de table 8001 h ne s'applique qu'à l'historique pré-1.9.0.
|
||
|
||
### 7.3b Override de modèle + prévision + auto-calibration (v1.2)
|
||
|
||
- **`modelOverride`** : paramètre optionnel de `concentrationOfDose` / `e2At` /
|
||
`computeCurve` qui force ESE ou TFS pour les traitements par profil — le graphique
|
||
dessine les deux modèles côte à côte depuis le même traitement (Bateman non concerné :
|
||
les deux séries y sont identiques).
|
||
- **`generateForecastDoses(treatment, doseLogs, toMs, nowMs)`** : projette les doses à
|
||
venir = dernière dose réelle + k × `forecastIntervalDays`, **strictement après
|
||
`nowMs`** (v1.2.7 : les créneaux déjà passés — cas d'un OUBLI — sont sautés au
|
||
rythme configuré, sinon faux pic dans l'historique ; un RETARD décale toute la
|
||
prévision) ; dose = standard du traitement, ester = override de la dernière
|
||
injection. Jamais persistées : uniquement passées à `computeCurve` par le
|
||
ChartScreen quand le chip « Prévision » est actif.
|
||
- **`autoCalibrated(treatments, doseLogs, labs, tConfig)`** : option « Calibration
|
||
automatique » (v1.2.1+) — renvoie `esterScales` (facteur **par ester**, cf §7.6)
|
||
+ `tKPerEster` (k T **par ester**, cf §7.5) recalculés depuis les labs ;
|
||
`treatments` et `tConfig` **inchangés** (fallback par ester absent = valeurs
|
||
stockées). HomeScreen et ChartScreen branchent dessus quand l'option est active.
|
||
|
||
### 7.4 Bateman (gel/patch/oral)
|
||
|
||
`C(dt) = (F·D·ka/(ka−ke))·(e^(−ke·dt) − e^(−ka·dt))` ; cas dégénéré ka≈ke :
|
||
`F·D·ke·dt·e^(−ke·dt)`. **`computeKa`** résout `ln(ka/ke) = (ka−ke)·Tmax` par bisection
|
||
(50 itérations, bornes `ke×1.001 … ke×1000`) — **direction corrigée** (cf §14 : `eq > 0`
|
||
⇒ la racine est **au-dessus** de mid ⇒ `lo = mid`).
|
||
|
||
### 7.5 Courbe T (empirique)
|
||
|
||
`T(t) = floor + (base − floor) / (1 + k(ester actif) · E2_calibrée(t))` [ng/mL].
|
||
Défauts `TConfig` : base 6.0, floor 0.2, k 0.19 (→ T≈0,4 à E2≈150). **Non issu du
|
||
`.ods`** (qui ne modélise pas la T) — modèle d'inhibition simple, étiqueté
|
||
« estimation » partout.
|
||
|
||
- **k par période d'ester** (v1.2.3) : la suppression T diffère selon l'ester
|
||
(valerate = pics hauts et courts, enanthate = plateau plus doux) →
|
||
`computeTKPerEster` attribue chaque lab T à la période de la dernière dose ≤ lab
|
||
et k = **médiane** des k de cette période ; la courbe utilise à chaque instant le
|
||
k de l'ester **actif** (`activeEsterAt`, curseur sur les doses triées dans
|
||
`computeCurve`), fallback = `tConfig.k` stocké.
|
||
- Formule : `k_i = ((base−floor)/(T_lab − floor) − 1)/E2_est(t_lab)`, garde
|
||
k ∈ (1e-4, 10). ⚠️ L'E2 utilisée est la version **calibrée** (scalePerEster) —
|
||
calibrer k contre une E2 brute faussait les k (corrigé v1.2.3).
|
||
- `computeTConfigCalibration` (k global unique) reste pour le bouton manuel
|
||
« Calibrer avec les analyses » des Paramètres.
|
||
|
||
**Unités** : les labs T peuvent être saisis en ng/mL, ng/dL, ng/L ou nmol/L —
|
||
`convertTToNgMl(value, unit)` normalise (ng/dL ÷100, ng/L ÷1000, nmol/L ×0,2884,
|
||
pg/mL ÷1000 défensif) ; appliqué à la calibration ET au rendu du chart (sinon l'axe
|
||
T est faux d'un facteur 100, bug réel remonté par l'utilisatrice : labs 33/44 ng/dL).
|
||
|
||
### 7.6 Calibration (v1.2.1 : PAR PÉRIODE D'ESTER pour l'auto)
|
||
|
||
**PORTÉE (v1.4.8, fix #60)** : le facteur est calculé PAR MODÈLE PK — le
|
||
graphique calcule une `AutoCalibrated` par modèle affiché (ESE/TFS/WHS, via
|
||
`autoCalibrated(modelOverride = …)`) : chaque courbe est calibrée avec la
|
||
prédiction DE SON modèle et passe par les labs quel que soit le modèle
|
||
superposé (test `calibration is per model`). Home (sans courbes superposées)
|
||
conserve la calibration du modèle STOCKÉ. HISTORIQUE : v1.4.7 partageait UN
|
||
seul facteur (calculé depuis le modèle stocké) entre toutes les courbes —
|
||
absurde dès qu'un traitement changeait de modèle (×2,21 sur WHSAH).
|
||
|
||
**GARDE DE SIGNIFICATIVITÉ (v1.4.9, fix #61)** : un lab ne calibre une
|
||
injection que si la prédiction du modèle à son instant reste **≥ 15 % du
|
||
maximum de prédiction déjà observé** (`labIsSignificant` — labs traités par
|
||
timestamp croissant, les 3 pipelines : manuel, échelles par ester, k T).
|
||
Au-delà de la fenêtre d'action (prédiction résiduelle), le lab reflète
|
||
AUTRE CHOSE : une dose EEn de test logguée en janvier + des labs de
|
||
janvier-mars produisaient des ratios ×3,3 → ×391 et la médiane tombait à
|
||
×2,21 (dans les DEUX pipelines). Résultat : facteur WHSAH réel = 0,55.
|
||
- chaque lab est **attribué à la période d'injection dans laquelle il tombe** =
|
||
dernière dose E2 ≤ lab (une prise de sang reflète d'abord l'injection qui précède) ;
|
||
- ratio = lab ÷ prédiction **non calibrée** (toutes doses superposées, scaleFactor forcé 1) ;
|
||
- facteur final par ester = **médiane** des ratios de sa période (EV/EU/EEN) ;
|
||
- application : `e2At`/`computeCurve` acceptent `scalePerEster: Map<String, Double>?` —
|
||
chaque dose est scalée par le facteur de **son** ester (`doseEster`, override compris),
|
||
fallback = `scaleFactor` stocké du traitement pour les esters sans lab.
|
||
- **Pourquoi** : un facteur unique par traitement mélangeait les périodes (labs valerate
|
||
mesurés contre une prédiction enanthate → ratio aberrant → courbes gonflées à
|
||
250–375 pg/mL, remontée v1.2.0). Cas vérifié sur données réelles : EEn 5 mg tous les
|
||
6–7 j (t½ ≈ 6,7 j) → accumulation ×2 → ~270 pg/mL calibré, cohérent labs 300/250 ;
|
||
non calibré ≈ 367.
|
||
- `autoCalibrated()` renvoie `AutoCalibrated(treatments **inchangés**, tConfig recalibré,
|
||
esterScales, calibratedEsters, tRecalibrated)` — écrans : `scalePerEster = effectiveAuto?.esterScales`.
|
||
|
||
**Manuelle** (`computeScaleFactor`, bouton « Calibrer avec les analyses » dans
|
||
l'éditeur de traitement) : facteur **unique par traitement** (médiane lab ÷ prédiction,
|
||
garde prédiction > 0,5), écrit le `scaleFactor` stocké. ⚠️ Limite documentée : en cas
|
||
de changement d'ester dans un même traitement, la manuelle mélange les périodes —
|
||
préférer l'auto-calibration dans ce cas.
|
||
|
||
**T** (`computeTConfigCalibration`) : `k_i = ((base−floor)/(T_lab − floor) − 1)/E2_est`,
|
||
garde k ∈ (1e-4, 10), **médiane** ; labs normalisés via `convertTToNgMl`.
|
||
|
||
### 7.7 API du moteur
|
||
|
||
`levelAt / currentLevel / computeCurve(start, end, step=1h, tConfig) / e2At /
|
||
testosteroneAt / computeScaleFactor / computeTConfigCalibration /
|
||
nextReminderFireMs(treatments, doseLogs, now) / nextReminderFireFor /
|
||
batemanParams / concentrationOfDose / computeKa / doseEster / isInjectionRoute`.
|
||
Type de retour : `LevelPoint(timestamp, e2, t)`.
|
||
|
||
|
||
### 7.8 Modèle TFS V3C (v1.4.0) — `pk/TransfemScienceModels.kt`
|
||
|
||
Le modèle « Transfem Science » n'est PLUS dérivé des tables ODS : c'est la
|
||
**forme close exacte** du modèle à 3 compartiments de la méta-analyse
|
||
(https://transfemscience.org/articles/injectable-e2-meta-analysis/), avec les
|
||
paramètres D/k1/k2/k3 publiés par le simulateur officiel
|
||
(transfemscience.github.io/injectable-e2-simulator — fichiers `ester-data.js`
|
||
/ `calc-curve.js`, valeurs = données scientifiques des études) :
|
||
|
||
```
|
||
Cp(t) = D·k1·k2 · [ e^(−k1·t)/((k1−k2)(k1−k3)) + e^(−k3·t)/((k1−k3)(k2−k3))
|
||
+ e^(−k2·t)·(k3−k1)/((k1−k2)(k1−k3)(k2−k3)) ] (t en JOURS)
|
||
```
|
||
|
||
- `sample(ester, dtHours)` = `Cp(t_jours) / fitDose(5 mg)` → pg/mL PAR mg ;
|
||
le moteur multiplie par la dose réelle, comme pour les tables ODS.
|
||
- Branchement dans `concentrationOfDose` : si `model == TFS` et que l'ester a
|
||
un V3C (EV, EEn, EU, EB, EC, ECS, PEP) → `TransfemScienceModels.sample` ;
|
||
sinon fallback tables ODS (Estrannaise, ou ester sans modèle).
|
||
- **Coupure moteur** (`cutoffHours`) : 10 demi-vies TERMINALES (t½ = ln2 /
|
||
min(k1,k2,k3)) — ex. EV 30 j, PEP 284 j — au lieu de la longueur de table.
|
||
- Compartiment k2 « ultra-rapide » (EU/PEP : k2 ~10⁵–10⁶ j⁻¹) : son terme
|
||
décroît instantanément, double précision suffisante (testé).
|
||
- Sanity checks épinglés (±1–3 %) : pics/t½ des Tableaux 9–10, AUC par mg
|
||
(EV 377,2 / EEn 436,6 / EC 430 / PEP 65,1 pg·j/mL/mg), état d'équilibre
|
||
EV 5 mg/7 j (Cmax 384 / Cmin 142 / Cavg 269, Figure 11 de l'article).
|
||
- ⚠️ EU (undécylate) : ajustement sur données d'étude LIMITÉES (cf article
|
||
§Limites) — précision moindre, conservé car c'est le meilleur disponible.
|
||
- Les valeurs des pics par mg restent cohérentes avec l'ancienne table ODS
|
||
(la table en DÉRIVAIT) : EV 59 pg/mL/mg @ 51 h, EEn 31,97 @ 156 h — le
|
||
changement est la FORME (queues exactes, pas d'arrondi 2 décimales).
|
||
|
||
### 7.9 Modèle WHSAH (v1.4.6) — `pk/WhsahModels.kt`
|
||
|
||
3ᵉ modèle PK, superposable, **demande exprès de l'utilisatrice** (« au même
|
||
niveau que les deux autres, pas de remplacement »).
|
||
|
||
- **Source** : le fit « license-free » publié par le WHSAH Collective dans
|
||
l'app open-source **Mona** (projet Flutter voisin `~/projects/mona`,
|
||
lib/data/model/graph_calculator.dart). Archéologie git de Mona : elle
|
||
embarquait les paramètres TFS (identiques aux nôtres) depuis mars 2026,
|
||
puis les a remplacés par ce fit indépendant — **pour des raisons de
|
||
licence** (les données du simulateur TFS portent un copyright « don't
|
||
reproduce » ; les valeurs numériques restent des résultats scientifiques
|
||
publiés, mais Mona a préféré re-fitter). Crédit : alix / WHSAH Collective.
|
||
- **Formule** (même tri-exponentielle, conventions Mona) :
|
||
`Cp = dose·F·auc·k1·k2·k3·(e^(−k1t)/((k1−k2)(k1−k3)) − e^(−k2t)/((k1−k2)(k2−k3)) + e^(−k3t)/((k1−k3)(k2−k3)))`
|
||
— normalisée PAR MG comme les autres modèles ; **F = biodisponibilité
|
||
explicite < 1** (0,62–0,76) vs ≈100 % chez TFS → courbes brutes ~24 %
|
||
plus basses (la calibration absorbe).
|
||
- **Params par ester** (F/auc/k1/k2/k3) : EEn 0.708/875.4/0.09441, 3.354, 0.4078 ·
|
||
EV 0.764/621.3/0.2230, 17.62, 1.305 · EB 0.723/889.9/0.5220, 521.9, 5.223 ·
|
||
EC 0.687/554.5/0.0880, 17.95, 0.7177 · ECS 0.687/852.6/0.0973, 218.67, 6.624 ·
|
||
EU 0.618/385.8/0.02189, 183.4, 1.564. **PEP non couvert** (retiré du fit).
|
||
- **Différences vs TFS** (5 mg, épinglé par `WhsahModelsTest` fidèle à
|
||
Mona ±2 %) : EV pic 367 @ 1,7 j (TFS 295 @ 2,1 j, +25 %) ; EEn 188 @ 5 j
|
||
(TFS 160 @ 6,5 j) ; **EEn à J+1 : 70 vs 22 pg/mL (×3)** ; t½ EEn 7,3 j vs
|
||
4,5 j. La MONTÉE rapide + le plateau haut/long de WHSAH expliquent que
|
||
les deux apps « ne montrent pas la même chose » le lendemain d'une
|
||
injection (cf #55).
|
||
- **Dispatch** (`concentrationOfDose`) : `WHSAH && WhsahModels.hasModel` →
|
||
`WhsahModels.sample` (avant le fallback tables ODS) ; `cutoffHours` =
|
||
10 × t½ terminale (EEn ≈ 73 j). Ester sans modèle → 0.
|
||
- UI : 3ᵉ toggle du graphique (E2 vert plein, T violet pointillé —
|
||
`ChartWhs`/`ChartTWhs`), 6 presets, dropdown modèle à 3 choix,
|
||
`Esters.choicesForModel` (TFS=7 / WHS=6 / ESE=3 — testé).
|
||
|
||
### 7.10 « Tracé labs » (v1.5.0) — `pk/LabTrajectoryModel.kt`
|
||
|
||
Courbe hybride ancrée sur les labs — **demande v1.5.0, débattue avec
|
||
l'utilisatrice** (hésitation assumée, cf §2) : « biologiquement plausible
|
||
MAIS passant par tous les points ».
|
||
|
||
- **Formule** : `courbe(t) = M(t) × ρ(t)` où
|
||
- `M(t)` = niveau du modèle PK BRUT (scaleFactor forcé 1, modèle stocké
|
||
ou `modelOverride`) — porte toute la physiologie entre les labs
|
||
(Tmax, queues, accumulation) ;
|
||
- `ρ_k = lab_k / M(t_k)` à chaque lab ; ENTRE deux labs consécutifs,
|
||
ρ interpole LOG-LINÉAIREMENT :
|
||
`ρ(t) = exp(lnρ_k + f·(lnρ_{k+1} − lnρ_k))` — morphing d'amplitude
|
||
multiplicativement continu (jamais de saut vertical) ;
|
||
- passage EXACT garanti : courbe(t_k) = M(t_k)·ρ_k = lab_k (épinglé).
|
||
- **Garde anti-#61** : labs croissants + `labIsSignificant` — un lab hors
|
||
fenêtre d'action (dose de test de janvier, labs tardifs) N'EST PAS un
|
||
ancrage (reproduction épinglée dans LabTrajectoryModelTest).
|
||
- **E2 uniquement** : les labs T sont trop rares + unités variées
|
||
(bug #26) — hors scope (choix v1.5.0). Pas de courbe T pour la série
|
||
(`tStyle = null` → rien de dessiné, miroir CurveChart).
|
||
- **Fenêtre = [1er lab significatif ; dernier lab] ∩ fenêtre du graphique** —
|
||
toujours bornée aux labs ; vieillir après le dernier lab = rien dessiné
|
||
(l'app n'invente pas de niveau). ⚠️ v1.6.0 : ce comportement reste le
|
||
DÉFAUT — avec le chip « Prolonger », la courbe peut se PROLONGER au-delà
|
||
du dernier lab (ρ constant, horizon = extinction du modèle — cf
|
||
§7.10.bis).
|
||
- < 2 labs significatifs → courbe vide (un lab seul = pas d'intervalle ;
|
||
un point isolé ne définit pas de tracé) — et donc jamais de prolongation.
|
||
- **Indépendante de la calibration** : elle EST sa propre calibration
|
||
continue (ratio brut, `scaleFactor` stocké ignoré ET `scalePerEster`
|
||
ignoré — l'inverse serait une DOUBLE correction, épinglé par test).
|
||
Conséquence : les modèles rapprochés au sein du même ancrage CONVERGENT
|
||
(le mi-chemin ESE vs WHS colle à ~2 % — `modelOverride` garde une
|
||
empreinte intermédiaire, testé avec le seuil du test moteur WHS).
|
||
- **Hors pics/creux** (chip "Pics / creux") : la série passe
|
||
`showExtrema = false` — ses extrema reflèteraient l'interp ρ, pas la
|
||
physiologie du modèle.
|
||
- **UI** : chip « Tracé labs » / "Lab track" dans la RANGÉE MODÈLES (4ᵉ
|
||
chip — scroll horizontal CONFINÉ à la rangée ; constat v1.5.0 en émulateur :
|
||
le 4ᵉ chip était coupé hors fenêtre avec une Row figée). OFF par défaut.
|
||
v1.6.0 : 5ᵉ chip « Prolonger » (off par défaut, désactivé tant que
|
||
Tracé labs est off) — prolongation au-delà du dernier lab (§7.10.bis).
|
||
- ⚠️ **Leçon #63 (v1.5.0, émulateur)** : la clé "LAB" dans la liste `curves`
|
||
doit être EXCLUE de la boucle de légende (`when (model)` — le `else`
|
||
= TFS imprimerait la légende TFS une 2ᵉ fois). Série → couleur dédiée
|
||
`ChartLabTrajectory` #C2185B dashed. v1.6.0 : la clé "LABX" (partie
|
||
prolongée) a la MÊME exigence — branche explicite dans le `when`, sinon
|
||
le `else` (= TFS) imprimerait la légende TFS une 2ᵉ fois.
|
||
- **INVARIANT** : cette courbe n'entre JAMAIS dans `levelAt` — accueil,
|
||
seuils d'alerte, rappels utilisent toujours le modèle calibré (§9.bis).
|
||
|
||
### 7.10.bis Prolongation au-delà du dernier lab (v1.6.0) — `pk/LabTrajectoryModel.kt`
|
||
|
||
**Demande v1.6.0** : permettre au tracé labs de s'étendre au-delà du
|
||
dernier lab, en simulant à partir des labs précédents, du dosage et du
|
||
type d'ester injecté, et de l'évolution classique de l'ester. Le constat
|
||
v1.5.0 : la fenêtre s'arrêtait AU dernier lab — précisément la période la
|
||
plus récente (entre la dernière prise de sang et maintenant) était
|
||
invisible sur la courbe de comparaison.
|
||
|
||
**Recette** : `extendBeyondLastLab = true` (paramètre de
|
||
`computeLabAnchoredCurve`, chip `Prolonger` — off par défaut, désactivé
|
||
tant que Tracé labs est off). La borne droite de la fenêtre devient
|
||
`min(demande, horizon)` et **la boucle de calcul est INCHANGÉE** :
|
||
`ratioAt` retournait déjà ρ_last au-delà du dernier ancre (garde
|
||
défensive v1.5.0) — élargir la fenêtre EST la prolongation. Continuité au
|
||
point de suture garantie par construction (ρ(t_last) = ρ_last exactement,
|
||
constant à droite comme l'interpolation aboutit à ρ_last à gauche).
|
||
|
||
- **Formule au-delà du dernier lab** : `courbe(t) = M(t) × ρ_last` où
|
||
- `M(t)` = le modèle PK brut : porte l'« évolution classique de l'ester »
|
||
(Tmax, queues, accumulation) ET LES DOSES LOGUÉES après le dernier lab —
|
||
une nouvelle injection (override d'ester compris : EV après des labs
|
||
EEn, etc.) fait repartir la courbe en pic à l'amplitude calibrée
|
||
(épinglé par test `une dose apres le dernier lab refait monter la
|
||
courbe`) ;
|
||
- `ρ_last` = ratio lab ÷ prédiction au dernier lab significatif, supposé
|
||
CONSTANT. **Pourquoi pas une extrapolation de pente** : la pente entre
|
||
deux labs est un morphing entre deux corrections d'amplitude, pas une
|
||
tendance physiologique — l'extrapoler divergerait (un ratio qui monte
|
||
de 5 %/jour finirait en facteurs ×10 inventés). ρ_last = le meilleur
|
||
estimateur disponible, même sémantique que la calibration classique
|
||
(le facteur stocké s'applique aux doses futures).
|
||
- **Horizon (`extensionHorizonEndMs`, internal)** : dernière dose E2 +
|
||
`cutoffHours(son traitement)` — `cutoffHours` a été rendu PUBLIC (v1.6.0)
|
||
pour que la prolongation partage la MÊME sémantique d'extinction que le
|
||
moteur (10 t½ terminales V3C/WHS, fin de table ODS, 30 t½ Bateman). Au-delà,
|
||
M(t) = 0 : dessiner plus serait une ligne à zéro inventée. Retourne
|
||
`null` (pas de prolongation) si aucune dose E2 ou si le modèle est DÉJÀ
|
||
éteint au dernier lab.
|
||
- **Split visuel "LAB" / "LABX" (ChartScreen)** : `AnchoredCurve` expose
|
||
`lastAnchorMs` (timestamp du dernier ancre, `null` si < 2 ancres) ;
|
||
l'écran splitte la série au dernier ancre — "LAB" = partie ancrée (rose
|
||
plein de la v1.5.0), "LABX" = partie prolongée (MÊME couleur atténuée
|
||
α 0,55, dashed) avec SA légende (`legend_lab_track_extend` FR/EN) :
|
||
« estimation, plus ancrée ». L'estimation ne peut pas se confondre avec
|
||
le mesuré.
|
||
- **AVERTISSEMENT (demande v1.6.0)** : quand la partie prolongée est
|
||
visible, un texte s'affiche sous la légende
|
||
(`lab_track_extend_warning` FR/EN) : « simple simulation, sans garantie
|
||
de correspondre au réel — extrapole ton modèle à partir de tes résultats
|
||
de laboratoire, qui peuvent eux-mêmes être erronés ; fie-toi à ta
|
||
prochaine prise de sang ». Même condition d'affichage que la légende
|
||
LABX (`labExtendVisible`) : il ne peut jamais y avoir de courbe
|
||
prolongée sans son avertissement.
|
||
- **Leçon #63 réappliquée** : la branche `"LABX" ->` du `when` de légende
|
||
est EXPLICITE et AVANT le `else` — sinon la légende TFS serait imprimée
|
||
une 2ᵉ fois (le `else` = TFS).
|
||
- **Garde #61 inchangée** : seuls les labs significatifs ancrent — un lab
|
||
tardif hors fenêtre d'action ne prolonge PAS la courbe et n'est jamais
|
||
traversé (épinglé par test).
|
||
- **Comportement par défaut inchangé** : flag absent ou `false` =
|
||
v1.5.0 bit-compatible (testé : points identiques).
|
||
- **Limites assumées (documentées, pas des bugs)** :
|
||
- changement d'ESTER après le dernier lab : l'amplitude du nouvel ester
|
||
reste calibrée par le ρ de l'ancien — même limite que la calibration
|
||
classique (pas de lab de la nouvelle période → fallback) ;
|
||
- les doses PRÉVISIONNELLES ne participent jamais : la projection future
|
||
reste la série « Prévision » (modèles calibrés) — le tracé labs ne
|
||
consomme que des doses RÉELLES ;
|
||
- **INVARIANT v1.5.0 étendu** : la partie prolongée non plus n'entre
|
||
JAMAIS dans `levelAt` (display-end only, série "LABX" purement visuelle).
|
||
- 8 nouveaux tests dans `LabTrajectoryModelTest` (191 verts au total).
|
||
|
||
### 7.11 Recommandation de prochaine prise de sang (v1.8.0) — `pk/LabTiming.kt`
|
||
|
||
Carte « Prochaine prise de sang (suggestion) » de la page Analyses
|
||
(miroir web : `js/pk/lab-timing.js`). Recommande **le premier creux
|
||
prévisionnel STABILISÉ** — deux idées pharmacocinétiques :
|
||
1. la prise la plus informative/comparable pour un ester injectable est au
|
||
**creux juste avant l'injection suivante** (le pic varie, le creux reflète
|
||
le niveau de fond) ;
|
||
2. un creux n'interprétable que si le régime est **stabilisé** : 5 × t½
|
||
terminale après le dernier changement (≈ 97 % de l'équilibre).
|
||
|
||
- **Traitement porteur** : ESTRADIOL + injectable (`usesProfileModel`) +
|
||
**actif** + Posologie > 0. Sinon `null` → carte cachée ; et si un
|
||
injectable E2 actif est SANS Posologie, l'UI affiche l'**invite**
|
||
([LabTiming.shouldSuggestPosology], demande v1.8.0 : rendre découvrable le
|
||
lien Posologie → recommandation). Les deux encarts sont exclusifs.
|
||
- **t½ terminale** : TFS/WHSAH analytiques (`terminalHalfLifeDays`) ;
|
||
**Estrannaise** : lue dans la table par
|
||
[PKProfileStore.terminalHalfLifeDays] (refactor v1.8.0 : la pente
|
||
log-linéaire utilisée par l'extrapolation de `sample()` — dernier point
|
||
≥ 1 % du pic, 48 h précédentes — est partagée).
|
||
- **Début du régime courant** (v1.8.1, **correction du proxy v1.8.0** —
|
||
critique : « changé d'ester, de dosage ET de posologie, et l'app disait
|
||
stabilisée depuis février ») : le régime courant = la **séquence terminale
|
||
de doses** où (a) l'**ester effectif** et la **dose (mg)** sont identiques
|
||
à la dose la plus récente, ET (b) **l'écart entre doses consécutives
|
||
reste dans l'intervalle de Posologie ± 24 h** (v1.9.7 — la comparaison
|
||
exacte v1.8.1 cassait le régime à chaque variation d'heure de log et
|
||
faisait fuir le creux recommandé ; « je m'injecte le même jour, à l'heure
|
||
près » — un vrai changement de créneau, ex. 2 j au lieu de 7 j, reste
|
||
hors fenêtre). Stabilisation = régimeStart +
|
||
5 × t½. Conservative assumée : des intervalles chaotiques maintiennent la
|
||
carte « non stabilisée » (pharmacocinétiquement vrai — le trough n'est
|
||
comparable que sur un intervalle régulier).
|
||
- **Creux** (**fix #68 v1.10.0**) : niveau PRÉ-INJECTION du créneau —
|
||
dernier point de la courbe E2 prévisionnelle (doses réelles +
|
||
[PharmacokineticEngine.generateForecastDoses], pas 1 h) juste avant
|
||
l'injection (principe 1). ⚠️ L'ancien « minimum de la fenêtre entière »
|
||
tombait juste APRÈS l'injection précédente pour les esters à montée
|
||
lente (EEn : pic ~J+5 ≈ intervalle 7 j → creux d'absorption) : date
|
||
antérieure à la stabilisation + texte « juste avant ton injection » faux
|
||
de plusieurs jours. Pour EV (t½ courte) le point pré-injection EST le
|
||
minimum de fenêtre → comportement inchangé. FORME brute (sans
|
||
calibration — le facteur ne déplace pas le minimum) ; horizons : au
|
||
moins 3 créneaux, étendu pour couvrir la stabilisation, borné à 12.
|
||
- **Filtres** : creux ≥ now + 6 h ; creux > dernière prise de sang (jamais
|
||
recommander un creux déjà mesuré) ; **creux ≥ stabilisation** (sinon
|
||
creux du créneau suivant — décision v1.8.0 « sauter » ; depuis #68 le
|
||
garde s'exprime sur le creux lui-même, instant réellement proposé).
|
||
- **UI** : LabsScreen — calcul `produceState` (Dispatchers.Default) + tick
|
||
minute ; carte avec creux exact (« At the estimated trough »), créneau
|
||
associé (+ mention « ou simplement la veille »), statut de stabilisation,
|
||
disclaimer. Stabilisation hors horizon (ester ultra-long) → carte cachée.
|
||
- Tests `LabTimingTest` (17 — v1.9.7 : 2 tests de tolérance ± 24 h ;
|
||
sémantique pré-injection inchangée par le fix #68) + régressions n°5 et
|
||
**n°6** (data-driven `backup-v1.9.8.json` : invariant « creux ≥
|
||
stabilisation » balayé sur `now` ± 60 j, pas 6 h).
|
||
|
||
### 7.12 Modèle Estrannaise ANALYTIQUE + nuage MCMC (v1.9.0) — `pk/EstrannaiseModels.kt`, `pk/EstrannaiseCloud.kt`
|
||
|
||
**DÉCISION v1.9.0** (utilisatrice) : « tous les modèles ont maintenant leurs
|
||
sources originelles — on abandonne complètement les liens avec le fichier
|
||
ODS, qui lui-même était une extrapolation de ces mêmes sources ». TFS
|
||
(v1.4.0) et WHSAH (v1.4.6) étaient déjà analytiques ; **l'Estrannaise le
|
||
devient à son tour**.
|
||
|
||
- **SOURCE** : https://github.com/WHSAH/estrannaise.js/ (site estrannaise) —
|
||
modèle 3 compartiments `dB/dt=−k1·B ; dEE/dt=k1·B−k2·EE ; dE2/dt=k2·EE−k3·E2`
|
||
en forme close : `C(t) = dose·d·k1·k2·[e^(−k1t)/((k1−k2)(k1−k3)) −
|
||
e^(−k2t)/((k1−k2)(k2−k3)) + e^(−k3t)/((k1−k3)(k2−k3))]` (t en JOURS).
|
||
Paramètres publiés par ester : EV [478, 0,236, 4,85, 1,24], EU
|
||
[471,5, 0,01729, 6,528, 2,285], EEn [191,4, 0,119, 0,601, 0,402], EC
|
||
[246, 0,0825, 3,57, 0,669], EB [1893,1, 0,67, 61,5, 4,34], EUCS
|
||
[16,15, 0,046, 0,022, 0,101] (nouveau code ester v1.9.0 : undécylate
|
||
suspension cristalline « EUn casubq »).
|
||
- **FIDÉLITÉ (vérifiée AVANT débranchement)** : la forme close reproduit les
|
||
tables ODS **RMS 0,00** sur 0→200 j (EV/EU/EEn), écart de pic ≤ 0,1 % —
|
||
l'ODS n'était que l'échantillonnage horaire de ces formules. Épinglé par
|
||
`EstrannaiseModelsTest` (Android) et `estrannaise-models.test.js` (web).
|
||
- **Débranchement runtime** : `concentrationOfDose` dispatche ESE →
|
||
`EstrannaiseModels.sample` (plus de fallback `PKProfileStore.sample`) ;
|
||
`cutoffHours` ESE → 10 × t½ terminale analytique (plus la longueur de
|
||
table) ; `LabTiming` t½ ESE analytique ; `PKProfileStore.init` retirée
|
||
du démarrage. **L'asset `pk_profiles.json` quitte `src/main/assets`
|
||
(−550 Ko d'APK) pour `src/test/assets/`** : l'ODS survit UNIQUEMENT
|
||
comme référence des tests de fidélité. `PKProfileStore` est conservé
|
||
(lecteur pur) pour ces tests.
|
||
- **Quelles doses le nuage couvre-t-il (v1.9.2)** : toutes les doses E2 à
|
||
profil injectable dont l'**ester effectif** (override compris) est
|
||
couvert par le fit Estrannaise — indépendamment du `pkModel` stocké du
|
||
traitement (fix v1.9.2 : l'ancien filtre `pkModel stocké == ESE` laissait
|
||
le nuage vide dès qu'aucun traitement n'était stocké en ESE — la courbe
|
||
ESE affichée trace pourtant toutes ces doses via modelOverride). Les
|
||
doses des traitements inactifs sont incluses (§6.bis) ; l'oral Bateman
|
||
reste hors nuage (pas d'ester échantillonnable).
|
||
- **Couverture étendue** : ESE passe de 3 à **6 esters injectables**
|
||
(EV/EU/EEn/EC/EB/**EUCS**) — `Esters.choicesForModel` mis à jour (TFS
|
||
reste 7, WHSAH 6 ; ⚠️ TFS = liste EXPLICITE : EUCS n'a pas de V3C TFS).
|
||
- **NUAGE D'INCERTITUDE (exclusif ESE, demande v1.9.0)** : Estrannaise
|
||
publie le **posterior MCMC** de ses paramètres (313 échantillons
|
||
`(d, k1, k2, k3)` par ester — asset `mcmc_samples.json` ≈ 48 Ko, chargé
|
||
au démarrage). Chip `Nuage` du graphique (off par défaut, activable à
|
||
volonté, ACTIVABLE SEULEMENT si ESE est affiché — éteindre ESE coupe le
|
||
nuage) → [EstrannaiseCloud.compute] trace 32 courbes du posterior
|
||
(échelonnées, déterministes) superposant les doses des porteurs ESE.
|
||
Dessin en alpha faible SOUS les courbes ; hors échelles/labels/extrema
|
||
(une plage d'imprécision, pas des données à cadrer). Limites : les
|
||
contributions TFS/WHSAH ne sont pas dans le nuage (pas de posterior
|
||
publié pour eux) ; les patchs du repo (tw/ow) ne sont pas portés.
|
||
- **Web sync** : `js/pk/estrannaise-models.js` + `js/pk/estrannaise-cloud.js`
|
||
miroirs ; le fetch des tables ODS au démarrage web est remplacé par le
|
||
fetch MCMC (démarrage plus rapide) ; nuage dessiné par `chart-canvas.js`.
|
||
|
||
#### Presets & éditeur — les 6 esters sélectionnables à la création (v1.9.4)
|
||
|
||
- **`PKPresets.kt` (25 entrées)** : 3 presets ESE ajoutés — EC huile
|
||
(5 mg IM), EB (1 mg IM), EUCS suspension cristalline (10 mg SC, ester
|
||
exclusif à ce fit). Champs Bateman **informatifs** (t½ terminales des
|
||
fits : EC ≈ 202 h pour k3 0,669/j, EB ≈ 4 h pour k3 4,34/j, EUCS
|
||
≈ 756 h pour k2 0,022/j) — pas utilisés au calcul, le dispatch passe
|
||
par la forme close.
|
||
- **`TreatmentEditorScreen.kt`** : la liste des esters =
|
||
`Esters.choicesForModel(model)` (même helper que l'override du
|
||
DoseDialog — plus aucune divergence des listes). Au changement de
|
||
modèle, si l'ester courant sort de la couverture (EUCS + TFS par ex.),
|
||
il rebascule sur EV : aucun choix possible ne produit 0 pg/mL.
|
||
- **Garde épinglée** : `PKPresetsTest` (miroir web
|
||
`tests/presets.test.js`) — « aucun preset injectable hors couverture de
|
||
son propre modèle » ; ESE 6 / TFS 7 explicite (sans EUCS) / WHSAH 6.
|
||
- **Web sync v1.9.4** : mêmes presets + i18n ; 2 trous du portage v1.9.0
|
||
corrigés au passage (`Esters.EUCS` manquant dans `pk-engine.js`,
|
||
`choicesForModel` resté à 3 esters).
|
||
|
||
## 8. Tests unitaires
|
||
|
||
**274 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — **240 sans
|
||
les données de test locales** (cf §8.bis : les 6 classes de régression sont
|
||
skippées via `Assume`). Dépendance : JUnit 4.13.2.
|
||
Emplacement : `app/src/test/java/com/hormonetrack/`. Répertoire de travail d'exécution =
|
||
`app/` → l'asset est lu via `src/main/assets/pk_profiles.json` (fallback `app/src/…`).
|
||
|
||
- **`PKProfileStoreTest`** (8) : les 6 profils présents (8001 pts) ; pic EV_ese = 61,12
|
||
@45 h ; zéro avant injection ; interpolation stricte entre points (heures 100/101 —
|
||
le plateau 45/46 est plat, piège de test) ; modèle inconnu → 0 ; extrapolation
|
||
terminale décroissante ; esters longs mesurables à 8000 h ; **les 6 pics == valeurs ODS**
|
||
- **`PharmacokineticEngineTest`** (15) : ke = ln2/t½ ; **pic Bateman ≈ Tmax** (attrape la
|
||
bisection inversée) ; EV 4 mg → pic ≈ 4×61 pg/mL ; superposition ; linéarité du
|
||
scaleFactor ; anti-androgène → 0 en E2 ; modèle T monotone/borné ; calibration SF =
|
||
médiane (0,5/0,9/1,4 → 0,9) ; calibration nulle sans labs ; **calibration T récupère un
|
||
k planté (0,25)** ; grille horaire clampée à la 1ʳᵉ dose ; vide sans doses ; override
|
||
d'ester par dose (EV≫EU à 45 h) ; prochain rappel dans le futur ; levelAt combiné
|
||
- **`AutoBackupRetentionTest`** (5) : v1.7.0 — rétention des auto-backups :
|
||
garde les N plus récents, ne touche JAMAIS exports manuels/logs/étrangers,
|
||
noms malformés jamais supprimés, keep clampé ≥ 1.
|
||
- **`BackupGsonTest`** (3) : round-trip JSON complet (enums, IDs, notes, TConfig),
|
||
rétrocompatibilité v1 (backup sans settings), import des paramètres utilisateur
|
||
- **`RegressionUserCaseTest`** (6) : **régression épinglée sur les données réelles
|
||
exportées** par l'utilisatrice (backup JSON v1.0.0 : 1 traitement EEn/ESE 5 mg, 1 dose,
|
||
4 labs dont T en ng/dL). Vérifie : parsing du JSON réel, courbes non vides et
|
||
physiologiquement plausibles pour 24 h/7 j/30 j (attrape le bug #22 de casse EEn),
|
||
labs antérieurs à la 1ʳᵉ dose ignorés par la calibration SF, conversion ng/dL→ng/mL,
|
||
calibration T avec labs en ng/dL. **En cas de nouveau bug remonté par l'utilisatrice :
|
||
exporter le JSON, l'épingler ici, reproduire, corriger.**
|
||
- **`V120FeaturesTest`** (12) : v1.2.0→v1.2.7 — doses prévisionnelles (rythme 7 j depuis la
|
||
dernière dose réelle, liste exacte J+4/J+11/J+18/J+25 ; vide sans intervalle ou sans
|
||
doses ; ester override projeté), override de modèle (ESE ≠ TFS à 45 h pour EV ;
|
||
sans override = modèle du traitement), auto-calibration v1.2.1 (facteur **par ester**
|
||
depuis un lab planté, T recalibré, **originaux non modifiés** ; inchangée sans lab
|
||
utilisable), **attribution des labs par période d'ester** (EV calibré par les labs EV,
|
||
EEn par les labs EEn — le scénario valerate→enanthate de l'utilisatrice) et
|
||
application de `scalePerEster` par dose.
|
||
- **`RegressionUserCase2Test`** (6) : **2ᵉ régression épinglée sur données réelles**
|
||
(export v1.2.0 : 9 doses EEn/TFS 5 mg ~6-7 j, 8 labs dont un T "pg/mL" par erreur,
|
||
SF stocké 0,72, fréquence 6 j). Vérifie : parsing, **état d'équilibre EEn** (t½ ≈
|
||
6,7 j + doses ~6-7 j → accumulation ×2 → e2 ≈ 270 calibré, cohérent labs 300/250 ;
|
||
non calibré ≈ 367 = les « 375 » rapportés), facteur unique EEN plausible, lab T en
|
||
unité aberrante neutralisé, prévision 6 j exacte, auto-cal cohérente.
|
||
**Tout nouvel export utilisateur = un nouveau test de régression.**
|
||
- **`RegressionUserCase3Test`** (6) : 3ᵉ régression (export **v1.3.1** — mis à
|
||
jour à la v1.3.2) — le scénario **transition** : traitement **EV inactif**
|
||
(29 doses 2–8 mg, janvier→juillet) + traitement **EEn actif** (9 doses) +
|
||
**CPA oral** (anti-androgène), soit 3 traitements, 51 doses, 26 labs.
|
||
Vérifie : parsing, **l'inactif reste simulé** (bug v1.2.4 — le moteur reçoit
|
||
TOUS les traitements), calibration par période couvrant EV **et** EEN,
|
||
k T par ester, continuité de la courbe pendant la transition, niveau
|
||
actuel ~EEn équilibre.
|
||
|
||
- **`LabsGroupingTest`** (9) : v1.2.2 — regroupement de l'écran Analyses (paire E2+T
|
||
même timestamp ; timestamps différents séparés ; tri E2 avant T avant autres ;
|
||
ordre chronologique décroissant). v1.7.1 — **notes d'un groupe**
|
||
(`labNotesForDisplay`) : notes distinctes E2/T → DEUX lignes préfixées du
|
||
marqueur (fix « une seule note sur deux »), note identique → une seule
|
||
ligne (dédupliquée), note seule → brute (v1.0 préservé), vides/blanches
|
||
ignorées, aucune note → aucune ligne.
|
||
- **`ChangelogHelperTest`** (8) : v1.3.0 — comparaison SemVer **numérique**
|
||
(1.2.9 < 1.2.10 : la comparaison lexicographique serait fausse), extraction
|
||
de sections depuis la dernière vue, première installation (section courante
|
||
seule), plusieurs versions intermédiaires, versions non numériques.
|
||
- **`AppLogTest`** (4) : v1.3.1 — formatage des lignes horodatées, buffer
|
||
circulaire (600 → 500 dernières, bordures).
|
||
- **`HrtDurationTest`** (6) : v1.3.1 — jours depuis la 1re prise (0 si
|
||
futur/invalide), décomposition mois(30 j)/jours.
|
||
- **`ExportFileNamesTest`** (7) : v1.3.4/v1.7.0 — noms d'export (logs/backup),
|
||
règle anti-#48 (pattern horaire ⇒ LocalDateTime), auto-backup horodaté +
|
||
`parseAutoBackupTimestamp` (round-trip, non-auto-backups → null).
|
||
- **`CalendarRruleTest`** (3) : v1.3.0 — RRULE `FREQ=DAILY;INTERVAL=N`,
|
||
arrondi demi-supérieur des décimales (6,5 → 7), clamp ≥ 1 jour.
|
||
- **`ExtremaTest`** (6) : v1.2.3 — détection des pics/creux (`detectExtrema`) :
|
||
alternance stricte pic/creux en régime d'équilibre (4 doses hebdo → ≥ 4 extrema,
|
||
pic > creux voisin, valeurs dans les bornes), courbe monotone → vide, série plate →
|
||
vide, série trop courte → vide, **filtre d'amplitude** (sémantique zigzag : une
|
||
oscillation sous le seuil produit UN pivot, l'alternance complète apparaît quand le
|
||
seuil baisse), anti-corrélation E2/T (un pic d'E2 ≈ un creux de T).
|
||
- **`TransfemScienceModelsTest`** (5) : v1.4.0 — épingle le modèle TFS V3C sur
|
||
l'article : pics/t½ des Tableaux 9–10 (±1–2 %), AUC par mg (±3 %), état
|
||
d'équilibre EV 5 mg/7 j = Figure 11 (Cmax 384/Cmin 142/Cavg 269), esters
|
||
sans modèle → 0, dt ≤ 0 → 0. **Toute retouche de TransfemScienceModels.kt
|
||
passe par CE test** (c'est la fidélité au simulateur TFS qui est vérifiée).
|
||
- **`WhsahModelsTest`** (6) : v1.4.6 — fidélité au comportement **Mona**
|
||
(fit WHSAH) : pics EV 367,5 @ 1,69 j / EEn 187,9 @ 4,99 j (±2 %), t½ par
|
||
ester, linéarité par mg, PEP non couvert, `choicesForModel` ×3 modèles.
|
||
- **`WhsahEngineTest`** (6) : v1.4.6→v1.4.8 — dispatch WHSAH (override
|
||
depuis un traitement ESE, `pkModel = WHS`, coupure 10 × t½, PEP → 0) et
|
||
**3 courbes EV distinctes** (ESE/TFS/WHS — seuil 2 % : ESE et TFS sont
|
||
quasi identiques au pic par construction) ; + v1.4.8 **calibration PAR
|
||
MODÈLE** (fix #60) : chaque courbe calibrée avec la prédiction de SON
|
||
modèle passe par le lab (±2 %), échelles TFS ≠ WHS au même lab ; +
|
||
v1.4.7 **calibration universelle épinglée** (le facteur par ester
|
||
s'applique à TOUTES les courbes, indépendamment du modelOverride).
|
||
- **`ChartZoomTest`** (23) : v1.2.9 → v1.4.10 — pas d'échantillonnage
|
||
adaptatif, horizon + clamp de panoramique, `pointHoursBefore`,
|
||
extension de prévision, `xLabelTicks` (minuit LOCAL, fuseau CHOISI,
|
||
cas dégénérés), `defaultModelToggles` + **pan en cumul fractionnaire**
|
||
(`panDeltaHours`, fix #62 : les deltas de doigt < 1 h s'accumulent
|
||
jusqu'à franchir une heure entière — la vue 24 h ne bougeait jamais ;
|
||
deltas négatifs = troncature vers zéro, cas dégénérés).
|
||
- **`ScaleFactorWhsahReproTest`** (4) : v1.4.9 — épinglé sur le bug ×2,21
|
||
(#61) : facteur manuel WHSAH 0,55 (plus jamais 2,21), cohérence
|
||
auto↔manuel (±15 %), garde de significativité (séquence réelle 184→1 et
|
||
cas aux frontières).
|
||
- **`CalibrationPerModelTest`** (5) : v1.4.8 — **process calibration** :
|
||
les 3 modèles (ESE inclus) calibrés atterrissent sur le lab ; échelles
|
||
distinctes (ESE↔TFS ~2 %, WHS ~11 % — nuance documentée : la table ODS
|
||
dérive du même article) ; **délégation** `autoCalibrated(modelOverride)`
|
||
→ `computeEsterScaleFactors` (implémentation unique — Home et graphique
|
||
ne peuvent pas diverger) ; **k T par modèle** (k ∝ 1/E2 calibrée :
|
||
échelle ×2 → k ÷2 ; les k post-calibration convergent entre modèles —
|
||
même E2 cible) ; labs antérieurs à la 1ʳᵉ dose → échelles vides (par
|
||
modèle). Toute retouche du pipeline de calibration passe par CE test.
|
||
- **`ChartScreenSourceGuardTest`** (1) : v1.4.4 — **garde de SOURCE** contre
|
||
le bug #54 : vérifie que les vals de fenêtre du graphique sont exposées au
|
||
gestionnaire de gestes via `rememberUpdatedState` (alias `gesture*`) et que
|
||
la closure `detectTransformGestures` ne les lit jamais en direct (captures
|
||
figées). Le lint Compose ne détecte pas ce pattern — ce test est le filet.
|
||
- **`RegressionUserCase4Test`** (5) : v1.4.3/v1.4.5 — 4ᵉ régression épinglée sur
|
||
les données réelles (export v1.4.2, HORS dépôt) : parsing v2 avec
|
||
**settings** (seuils + auto-cal), scénario transition EV inactif → EEn
|
||
actif, **signature pharmacocinétique EEn** (contribution d'une dose à
|
||
J+1 < 12 % du total, plateau 48 h ≤ 25 % — c'est la réponse épinglée à
|
||
« pourquoi l'estimation ne remonte pas le lendemain »), pas de fausse
|
||
alerte à l'observation. **Tout nouvel export = une régression data-driven.**
|
||
- **`RegressionUserCase5Test`** (7) : v1.7.1 — 5ᵉ régression épinglée sur
|
||
les données réelles (export v1.7.0, HORS dépôt) : profil à 3 traitements
|
||
(CPA oral Bateman + EEn actif + EV inactif simulé, §6.bis), **le fix
|
||
« une note sur deux » épinglé sur la VRAIE paire** (dernière prise E2+T
|
||
avec deux notes distinctes → `labNotesForDisplay` en rend deux, préfixées ;
|
||
note identique → une seule), notes de DOSES préservées, plausibilité
|
||
moteur (niveau physiologique, courbe 30 j, auto-calibration couvre un
|
||
ester), prévision 7 j. **Tout nouvel export = une régression data-driven.**
|
||
- **`AlertsTest`** (12) : v1.4.2 — logique pure des seuils (`pk/Alerts.kt`) :
|
||
HIGH/LOW, comparaison STRICTE (valeur == limite → rien, éviter le
|
||
clignotement d'arrondi), seuil null → jamais d'alerte, cohérence haut > bas,
|
||
HIGH prime LOW en config incohérente (défensif), les deux marqueurs à la
|
||
fois (E2 puis T), **codec d'état** (`encodeState`/`parseState` — rétro-résistant
|
||
aux entrées malformées) et **décision de notification** (`shouldNotify` :
|
||
nouveau franchissement → notif, même état → pas de spam, retour à la
|
||
normale → pas de notif mais ré-arme).
|
||
- **`AlertsEngineTest`** (4) : v1.4.2 — câblage moteur → seuils : le niveau
|
||
évalué par la NOTIFICATION (`levelAt(scalePerEster)`) est celui de la
|
||
CARTE accueil (`computeCurve` dernier point, ±10 % de dérive horaire) ;
|
||
seuil à la moitié du niveau réel → HIGH, seuil au double → rien ; T bas
|
||
→ LOW en ng/mL ; l'état persisté ne contient que les marqueurs en alerte.
|
||
- **`TransfemScienceEngineTest`** (4) : v1.4.0 — câblage MOTEUR ↔ V3C :
|
||
EB (sans table ODS) routé en TFS → pic article 971 pg/mL @ 0,65 j ;
|
||
EB forcé en ESE (`modelOverride`) → 0 partout (comportement documenté :
|
||
presets EB/EC/ECS/PEP en TFS uniquement) ; PEP 32,5 mg → contribution
|
||
à 10 j ET 100 j (l'ancienne coupure 24 h du fallback table aurait donné 0)
|
||
et 0 au-delà de 10 × t½ (par design) ; `computeCurve(modelOverride=TFS)`
|
||
sur un traitement stocké ESE reproduit le pic V3C EV 295 — c'est le test
|
||
du dispatch `concentrationOfDose` + de `cutoffHours`.
|
||
- **`ReminderScheduleTest`** (8) : v1.4.0 — fix #52 : rappel sur la GRILLE
|
||
Posologie (dose il y a 2 j + intervalle 7 j → alarme dans 5 j à l'heure
|
||
choisie, PAS demain), heure déjà passée le jour du créneau → créneau
|
||
suivant, créneau manqué sauté (jamais dans le passé), fallback quotidien
|
||
sans Posologie ou sans doses, agrégat multi-traitements (le plus tôt
|
||
gagne), traitements inactifs/désactivés → null.
|
||
|
||
- **`EstrannaiseModelsTest`** (6) : v1.9.0 — modèle Estrannaise analytique :
|
||
FIDÉLITÉ aux tables ODS (maxAbs ≤ 0,01 / RMS ≤ 0,01 sur 0→200 j), pics
|
||
publiés reproduits (±0,2 %), cas dégénérés (taux égaux → limites sans
|
||
NaN), MCMC 313 × 6 esters (plage couverte), t½ analytique EV,
|
||
normalisation par mg + gardes (EUCS couvert).
|
||
- **`EstrannaiseCloudTest`** (5) : v1.9.0 — nuage d'incertitude : 32 courbes
|
||
à dispersion réelle (les posterior couvrent une plage), fenêtre respectée,
|
||
exclusivité ESE (TFS/oral → vide), sans dose → vide, nbCurves < 2 → vide.
|
||
- **`PKPresetsTest`** (6) : v1.9.4 — presets de création : les presets ESE
|
||
couvrent les 6 esters du fit (unicité, même ensemble que
|
||
`choicesForModel`), TFS (7) / WHSAH (6) inchangés, **garde anti
|
||
« 0 pg/mL »** (aucun preset injectable hors couverture de son modèle),
|
||
champs Bateman informatifs plausibles (t½ des fits).
|
||
- **`LabTimingTest`** (11) : v1.8.0/v1.8.1 — recommandation de prochaine
|
||
prise de sang : creux juste avant le créneau, saut au premier creux
|
||
STABILISÉ (5 × t½ ; régime non stabilisé → creux suivant), prise récente →
|
||
creux suivant, sans Posologie/oral → null, invite Posologie, Estrannaise
|
||
lit sa t½ dans la table ; v1.8.1 — changements récents (dose/intervalle/
|
||
ester) réinitialisent la stabilisation.
|
||
- **`LabTrajectoryModelTest`** (19) : v1.5.0 — « Tracé labs » (§7.10) :
|
||
passage EXACT sur chaque lab (fp 1e-6), ρ log-linéaire (fonction pure +
|
||
courbe recomposée via le moteur — M recalcule et ρ = geometric mean),
|
||
1 lab → vide, **garde #61** (lab tardif PAS un ancrage + fenêtre bornée
|
||
au dernier lab SIGNIFICATIF), labs avant 1ʳᵉ dose → 0 ancrage, fenêtre
|
||
clippée [labs ∩ demande], **indépendance du scaleFactor stocké**
|
||
(sinon double correction → courbes identiques sous SF 1,0 vs 0,55),
|
||
`modelOverride` (ESE vs WHS — divergence franche : ESE≈TFS quasi
|
||
identiques par CONSTRUCTION, seuil 2 % doc §8), labs T ignorés,
|
||
doublons de timestamp (2ᵉ ratio gagne), bornes défensives ratioAt.
|
||
v1.6.0 — **prolongation** (§7.10.bis) : identité `M × ρ_last` exacte
|
||
au-delà du dernier lab (10/20/30 j), suture exacte au dernier ancre,
|
||
horizon = cutoff du traitement (dernier point ≤ cutoff < dernier point + pas),
|
||
dose EV loguée après le dernier lab → pic de prolongation (> ×1,3 le
|
||
niveau pré-injection — la forme suit dosage + ester), flag off =
|
||
v1.5.0 bit-compatible, garde #61 sur la prolongation (le lab tardif ne
|
||
prolonge ni n'ancre), gardes du helper d'horizon (pas de dose / modèle
|
||
éteint → null), `lastAnchorMs` (null si < 2 ancres).
|
||
|
||
### 8.bis Données de test réelles : HORS dépôt (`local-test-data/`)
|
||
|
||
Les cinq classes de régression (`RegressionUserCase{,2,3,4,5}Test`) épinglent le comportement
|
||
du moteur sur les **exports réels** de l'utilisatrice. Ce sont des **données de santé
|
||
personnelles** : elles ne sont **pas versionnées**, pour ne rien divulguer dans le
|
||
dépôt (ni maintenant, ni si le repo devient public un jour).
|
||
|
||
- emplacement : `local-test-data/backup-v1.0.0.json`, `backup-v1.2.0.json`,
|
||
`backup-v1.2.3.json` (ancien export, plus consommé par un test),
|
||
`backup-v1.3.1.json`, `backup-v1.4.2.json`, `backup-v1.7.0.json` et
|
||
`backup-v1.9.8.json`
|
||
(copiés tels quels depuis l'export JSON de l'app) ;
|
||
`RegressionUserCaseTest` lit v1.0.0,
|
||
`RegressionUserCase2Test` lit v1.2.0, `RegressionUserCase3Test` lit
|
||
**v1.3.1**, `RegressionUserCase4Test` lit **v1.4.2** (premier export avec
|
||
les settings v2), `RegressionUserCase5Test` lit **v1.7.0** (profil à 3
|
||
traitements dont CPA oral ; paire E2+T avec DEUX notes distinctes —
|
||
épine le fix « une note sur deux » v1.7.1), `RegressionUserCase6Test` lit
|
||
**v1.9.8** (profil à 7 traitements, logs à heures variables ; épine le
|
||
fix #68 « creux avant stabilisation » — invariant balayé sur `now` ±60 j) ;
|
||
- `.gitignore` contient `local-test-data/` → jamais commités.
|
||
**GARDES DE CONFIDENTIALITÉ (rappel 7 sept. 2026, à chaque release)** :
|
||
1. `git check-ignore -v local-test-data/…` → la règle matche ;
|
||
2. `git ls-tree -r <tag> --name-only | grep local-test-data` → VIDE avant
|
||
publication ;
|
||
3. `git log --all -- local-test-data/` → vide (aucun commit n'y a jamais
|
||
touché — vérifié à la v1.4.3) ;
|
||
4. le scan de confidentialité (working tree + §8.bis dette historique) ;
|
||
5. les assertions des régressions sont DATA-DRIVEN (elles lisent le
|
||
fichier local) — JAMAIS de valeur de lab ni de timestamp réel en dur
|
||
dans le code (le scanner refuserait, et ce serait une fuite).
|
||
- les tests font `Assume.assumeTrue(file.exists())` dans le `@Before` : **sans le
|
||
fichier, la classe est IGNORÉE** (skipped, pas failed) — un clone neuf ou une CI
|
||
exécute 240 tests au lieu de 274 (v1.12.0 ; chiffres historiques : 44/87 à v1.3.x,
|
||
150/172 à v1.4.x, 160/183 à v1.5.0, 168/191 à v1.6.0, 177/200 à v1.7.0,
|
||
181/211 à v1.7.1, 190/220 à v1.8.0, 219/249 à v1.9.x, 225/259 à v1.10.x, 230/264 à v1.11.x) ;
|
||
- le workdir des tests Gradle est le dossier du module (`app/`) → les tests
|
||
cherchent les fichiers à plusieurs chemins (`../local-test-data/…` en premier) ;
|
||
- **pour les lancer** : exporter un backup JSON depuis l'app → l'enregistrer sous
|
||
le nom attendu dans `local-test-data/` → `./gradlew testDebugUnitTest` ;
|
||
- ⚠️ **ne jamais embarquer ces données dans les tests** (string inline dans le
|
||
code) : tout nouvel export réel → fichier gitignore + assertions data-driven ;
|
||
- **l'historique a été nettoyé avant le premier push** : les premiers commits
|
||
embarquaient les valeurs (tests + doc) → `git filter-branch --tree-filter` avec
|
||
un script d'anonymisation (timestamps décalés de +30 j, valeurs perturbées dans
|
||
la prose des docs), tags réécrits, refs purgeées.
|
||
⚠️ **Lint de dette connue (constat v1.3.3)** : le scanner travaillait sur une liste
|
||
de motifs figée ; depuis que `local-test-data/` s'enrichit de NOUVEAUX exports
|
||
(ex. backup-v1.3.1.json ajouté à la v1.3.2), la ré-extraction des valeurs fait
|
||
que le scan `--history` détecte des fragments JSON de labs **encore présents
|
||
dans l'historique des révisions v1.1.0 → v1.2.3** (tests `RegressionUserCase{,2}`
|
||
data-driven AVANT la migration fichiers gitignorés ; fragments de la forme
|
||
« value » + timestamp). Amplitude : quelques valeurs numériques de labs — les
|
||
mêmes ordres de grandeur figurent déjà en PROSE dans les docs publiés (§14 #23,
|
||
CHANGELOG 1.1.0). Le working tree est propre. **Décision utilisateur (6 sept.
|
||
2026)** : purge complète de l'historique NON relancée (les révisions sont déjà
|
||
répliquées sur les deux Gitea, aucune fuite nouvelle par push) ; la dette
|
||
(réécriture d'historique + re-tags + force-push des 2 remotes + re-publish des
|
||
releases) reste ouverte et doit être re-décisionnée si le dépôt passe PUBLIC.
|
||
|
||
**Ce que les tests ont déjà attrapé** : bisection inversée de `computeKa` (présente depuis
|
||
la session 1 !), plancher 0,01 des queues de profils, mapping silencieux du modèle inconnu.
|
||
**Toute modification du moteur passe par ces tests.** Suite envisagée : Robolectric
|
||
(UI/logic Android — les tests Compose existent depuis v1.9.8, cf §8.ter).
|
||
|
||
### 8.ter Tests UI Compose (`app/src/androidTest/`, v1.9.8)
|
||
|
||
**14 tests instrumentés** (`uitest/`) exécutés sur émulateur/appareil :
|
||
|
||
```bash
|
||
./gradlew connectedDebugAndroidTest # émulateur requis (ex. AVD hrt36)
|
||
```
|
||
|
||
- **`NavigationTest` (7)** : la barre d'onglets affiche les 5 entrées, chaque
|
||
onglet mène bien à son écran (preuves = éléments INCONDITIONNELS : chips de
|
||
plage « 24 h » du graphique, FAB « + » de Doses/Analyses/Traitements —
|
||
indépendants des données Room), et les Paramètres s'ouvrent depuis l'icône
|
||
de l'accueil ;
|
||
- **`ChangelogDialogTest` (3)** : dialog « Nouveautés » affiché quand la
|
||
version installée est plus récente que la dernière vue, absent sinon, et
|
||
**ne réapparaît pas après dismiss + redémarrage** (épinglage du fix v1.3.2
|
||
en test UI) ;
|
||
- **`SettingsScreenTest` (3)** : sections de l'écran Paramètres toutes
|
||
présentes + chips de langue littéraux (« Français »/« English ») +
|
||
**version installée = BuildConfig.VERSION_NAME** (le bug #46 — versions
|
||
jamais bumpées dans les tags — ne peut plus revenir silencieusement) ;
|
||
- **`LogDoseRequestTest` (1, v1.10.0)** : le dialog de dose s'ouvre UNE fois
|
||
au lancement avec les extras de notification, puis ne se rouvre JAMAIS
|
||
après fermeture + aller-retour d'onglets (épinglage du fix #69). Lancement
|
||
manuel avec intent (règle vide + ActivityScenario) ; BACK = fermeture
|
||
indépendante des libellés (UiDevice, dep `uiautomator:2.3.0`).
|
||
|
||
**Déterminisme** (les tests tournent contre l'app RÉELLE, données incluses) :
|
||
- permission `POST_NOTIFICATIONS` pré-grantée via `pm grant` shell AVANT le
|
||
lancement (sinon le dialog SYSTÈME masque l'app → timeout) ;
|
||
- dialog « Nouveautés » neutralisé en marquant la version vue
|
||
(`UiTestEnv.markChangelogSeen`) SAUF pour les tests qui le ciblent — ceux-ci
|
||
lancent l'activité MANUELLEMENT (`ActivityScenario` + `createEmptyComposeRule`)
|
||
pour régler le DataStore AVANT la lecture au démarrage ;
|
||
- assertions de texte via `targetContext.getString(...)` : résolues dans la
|
||
MÊME configuration que l'UI (per-app locales incluses) → indépendant de la
|
||
langue système et du réglage in-app FR/EN ;
|
||
- aucune assertion dépendante des données Room (listes/curbes) — les tests
|
||
restent verts sur émulateur vierge OU seedé (§16.ter).
|
||
|
||
⚠️ `assertExists`/`assertDoesNotExist` sont des MEMBRES de
|
||
`SemanticsNodeInteraction` depuis Compose 1.12 (plus des extensions à importer).
|
||
|
||
## 9. Système de rappels
|
||
|
||
`reminder/ReminderManager.kt` (+ `DoseActionReceiver.kt`, `ReminderReceiver`).
|
||
|
||
- `ReminderContract` : constantes + **fabrique unique `reminderIntent()`** pour schedule
|
||
ET cancel (même action = même PendingIntent — cf bug §14.3)
|
||
- `AlarmScheduler` :
|
||
- **`scheduleFor(treatment, doseLogs)` (v1.4.0, source de vérité = moteur)** :
|
||
la date du prochain déclenchement vient de
|
||
`PharmacokineticEngine.nextReminderFireFor` — **grille Posologie** si le
|
||
traitement a `forecastIntervalDays` + des doses (rappel UNIQUEMENT le jour
|
||
du créneau, à l'heure choisie — fix #52), sinon quotidien (comportement
|
||
historique, gel/oral ou app sans historique)
|
||
- alarmes **one-shot** (`setExactAndAllowWhileIdle`) : la suivante est
|
||
reprogrammée par `ReminderReceiver` (après notif) et `DoseActionReceiver`
|
||
(après « Pris ») — avant v1.4.0 la chaîne ne se recousait qu'au boot/au save
|
||
- permission **SCHEDULE_EXACT_ALARM** : bouton d'octroi dans Paramètres + éditeur
|
||
(`Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM`)
|
||
- `scheduleSnooze` (+1 h), `rescheduleAll(treatments, doseLogs)`
|
||
- `ReminderReceiver` : notif HIGH/REMINDER, 2 actions + tap → MainActivity
|
||
(`open_log_dose`, `treatment_id`) → Home ouvre le dialog pré-rempli ; requestCodes
|
||
PendingIntent = `id*10+{0,1,2}` ; notificationId = `id.toInt()` ; **puis
|
||
reprogramme le prochain déclenchement** (goAsync + IO).
|
||
**v1.10.0 — garde « créneau déjà honoré »** : au déclenchement,
|
||
`PharmacokineticEngine.hasDoseLoggedOnDay` (pur, jour calendaire LOCAL —
|
||
6 tests `ReminderLoggedDayTest`) consulte les doses : dose du jour déjà
|
||
loguée → **notification sautée** (prise saisie avant l'heure de rappel —
|
||
la notif ne sonne pas pour une prise faite) ; la re-programmation du
|
||
créneau SUIVANT reste exécutée dans tous les cas. Vérifié A/B sur
|
||
émulateur (`am broadcast -n …ReminderReceiver` : EEn avec dose du jour →
|
||
0 notif, Fluoxetine sans dose → 1 notif).
|
||
**v1.10.1 — instant PLANIFIÉ (`EXTRA_PLANNED_AT`)** : le garde évalue le
|
||
jour du CRÉNEAU PLANIFIÉ (embarqué dans l'intent de l'alarme par
|
||
`scheduleFor`, forwardé tel quel par le snooze), pas le jour du feu
|
||
réel — une alarme inexacte (`setWindow`) ou retardée (Doze) glissant
|
||
après minuit évaluait le mauvais jour (23:58 → 00:02). Vérifié émulateur
|
||
(broadcast avec `planned_at` daté d'hier → notification sautée).
|
||
- `DoseActionReceiver` (non exporté) : **« Pris »** → `goAsync()` + coroutine IO → insert
|
||
DoseLog (dose = extra ou standard) **+ reprogramme** (la dose loguée avance
|
||
la grille) ; **« Reporter 1 h »** → `scheduleSnooze` ; annule la notif
|
||
- `BootReceiver` : `goAsync()` + thread + **`runBlocking`** + one-shots
|
||
`getAllOnce()` (traitements) + `doseLogDao.getAllOnce()` → `rescheduleAll`
|
||
(jamais un Flow en runBlocking !)
|
||
|
||
**Événements d'agenda (v1.3.0, `reminder/CalendarEvents.kt`)** : switch dans
|
||
l'éditeur (sous Rappels) → insertion dans un **calendrier local dédié
|
||
« HormoneTrack »** (CalendarContract, ACCOUNT_TYPE_LOCAL) d'un événement
|
||
récurrent RRULE `FREQ=DAILY;INTERVAL=N` (N = Posologie, arrondi demi-supérieur
|
||
**explicite** : `floor(x + 0.5)` — kotlin.math.round arrondit les ties vers
|
||
l'entier PAIR !), début = prochaine occurrence à l'heure de rappel (ou 12:00).
|
||
L'id est stocké sur le traitement (`calendarEventId`, Room v3) ; au save :
|
||
supprimer + recréer (fiable) si changé, supprimer si désactivé. Permissions
|
||
WRITE_CALENDAR + READ_CALENDAR demandées à l'activation du switch.
|
||
Manifest : `POST_NOTIFICATIONS`, `SCHEDULE_EXACT_ALARM`, `RECEIVE_BOOT_COMPLETED`,
|
||
`VIBRATE`, `WRITE_CALENDAR`, `READ_CALENDAR`.
|
||
⚠️ **Permissions agenda (v1.3.3)** : WRITE_CALENDAR + READ_CALENDAR ont été
|
||
ajoutées au manifest seulement en v1.3.3 — elles étaient documentées ici
|
||
depuis v1.3.0 mais **jamais déclarées dans le XML** (bug #44, §14) : tout
|
||
jeton « le manifest contient X » doit être VÉRIFIÉ dans le fichier réel.
|
||
Sur la montre : remontée par Gadgetbridge **ou** Huawei Health (cf §17).
|
||
|
||
### 9.bis Alertes de seuil : `reminder/AlertNotifier.kt` + worker (v1.4.2)
|
||
|
||
- **Seuils** (`pk/Alerts.kt`, PUR) : `Thresholds` (E2 haut/bas pg/mL, T haut/bas
|
||
ng/mL, null = pas de limite), évaluation STRICTE (valeur == limite → rien),
|
||
`evaluateAll` (E2 puis T), validation haut > bas (`isCoherent`).
|
||
- **Worker WorkManager** : `AlertWorker` (CoroutineWorker) périodique **15 min**
|
||
(minimum WorkManager) + **one-time au save des seuils** (feedback immédiat) ;
|
||
planifié dans `HormoneTrackApp.onCreate` (`ExistingPeriodicWorkPolicy.KEEP`,
|
||
survit aux reboots — BootReceiver n'a RIEN à faire).
|
||
- **Calcul du niveau** : `levelAt(..., scalePerEster, tKPerEster)` — MÊME
|
||
calibration que la carte accueil (auto-cal incluse si l'option est active).
|
||
- **Anti-spam** : l'état des alertes déjà notifiées est persisté en DataStore
|
||
(`alert_notified_state` = `Alerts.encodeState`, ex. « E2:HIGH;T:LOW ») ;
|
||
`shouldNotify` : nouveau franchissement ou changement H↔L → notif, même
|
||
état → rien, retour à la normale → état effacé (ré-arme), pas de notif de retour.
|
||
- **Canal dédié** `hormonetrack_alerts` (IMPORTANCE_HIGH) : réglable
|
||
indépendamment des rappels ; notification avec texte multi-lignes (BigText),
|
||
tap → MainActivity. Garde défensive POST_NOTIFICATIONS (API 33+) journalisée.
|
||
- Journalisation AppLog complète (« notification : E2:HIGH... », « franchissement
|
||
inchangé », « retour à la normale », erreurs worker) — diagnostic à distance.
|
||
|
||
## 10. UI & navigation
|
||
|
||
- `HormoneTrackRoot` : NavigationBar 5 tabs (home/chart/doses/labs/treatments) + routes
|
||
`settings`, `treatment_edit/{id}` (-1 = nouveau) ; barre masquée sur ces 2 routes
|
||
- `HomeScreen` : bandeau gradient (TransSky→TransPink, discret), carte **niveau actuel**
|
||
(E2 ≈ X pg/mL, T ≈ Y ng/mL, delta vs 6 h — **v1.4.3 : comparaison au point le
|
||
plus proche de −6 h via `pointHoursBefore`, l'ancien code comparait ~24 h**,
|
||
bug #53), **cartes d'alerte v1.4.2** (si un
|
||
seuil est franchi : fond `errorContainer`, « ▲ E2 estimé ≈ X pg/mL — au-dessus
|
||
de ta limite (Y) », basées sur le DERNIER point de courbe = niveau affiché,
|
||
disclaimer « estimation, pas une mesure »), carte prochaine dose (**v1.4.1 :
|
||
delta en JOURS au-delà de 24 h — « 5 j 2 h · sam. 6 18:00 » via
|
||
`HrtDuration.daysAndHours` + `next_dose_days` ; en dessous, h/min**), chips de log rapide
|
||
(+ FAB), mini-chart 24 h (multi-séries via `ChartSeries`) **cliquable → écran
|
||
Graphiques** (v1.2.1) avec mini-légende E2/T ; données auto-calibrées si l'option est
|
||
active (`scalePerEster = effectiveAuto?.esterScales`) ; rafraîchissement `tick` 60 s
|
||
- `ChartScreen` (v1.2, le plus riche) : **calibration PAR MODÈLE** (v1.4.8,
|
||
fix #60 : `autoByModel` — une AutoCalibrated par modèle affiché, chaque
|
||
courbe reçoit SES échelles/k) ; **toggles de modèles initialement
|
||
alignés sur les traitements** (v1.4.7, fix #58 : `defaultModelToggles` sur
|
||
les pkModel des traitements à PROFIL PK — init unique au chargement ;
|
||
garde-fou TFS si aucun profil — les traitements Bateman sont tracés dans
|
||
chaque série) ;
|
||
- `ChartScreen` (v1.2, le plus riche) : **fenêtre temporelle v1.4.1** —
|
||
`endMs = now − panHours` avec `panHours` signé : > 0 = passé (tirer vers la
|
||
droite), **< 0 = futur** (tirer vers la gauche, uniquement avec la
|
||
prévision active, borné par `forecastHorizonHours` = 12 × Posologie borné
|
||
30 j–1 an). **Activer le chip Prévision ne déplace PAS la fenêtre** (la
|
||
projection s'étend au-delà et se parcourt au drag) ; désactiver pendant un
|
||
voyage futur → retour à maintenant ; clamp centralisé dans
|
||
`clampPanHours` (testé). Plages 24 h/7 j/30 j + **boutons zoom − / +**
|
||
(v1.2.9) ; **panoramique**
|
||
(`detectHorizontalDragGestures` — tirer vers la droite remonte dans le passé,
|
||
`panHours` borné à [0, âge de la 1ʳᵉ dose + plage], bouton « Revenir à maintenant ») ;
|
||
**toggles indépendants Estrannaise/TFS** → deux `computeCurve` avec `modelOverride`
|
||
superposées (E2 ESE bleu plein, E2 TFS turquoise, T ESE rose plein, T TFS rose
|
||
pointillé) ; **chip Prévision** (doses projetées via `generateForecastDoses`, horizon
|
||
= 2× le plus grand intervalle configuré, borné 7–30 j) ; **auto-calibration** branchée
|
||
sur les Paramètres ; légende dynamique ; labs T normalisés en ng/mL ; **toggle T =
|
||
masque aussi les labs T** (v1.2.2) ; **chip « Pics / creux »** (v1.2.3 : triangles ▲▼
|
||
aux extrema locaux de chaque courbe, via `detectExtrema` — E2 seuil 2 pg/mL, T seuil
|
||
0,02 ng/mL)
|
||
- `DosesScreen` : LazyColumn par jour (desc), **Δ jours depuis la dose précédente du même
|
||
traitement** (`intervalsByDoseId`, colonne « Interval (d) » du `.ods`), suppression
|
||
avec confirmation, FAB → `DoseDialog` (création), **tap sur la ligne → édition** ;
|
||
**v1.12.0** : **icône de forme de prise** en tête de ligne (`routeIcon` pur —
|
||
pilule/seringue IM+SC teinte tertiary pour SC/goutte/sparadrap/compte-gouttes)
|
||
et **encart latéral « prise de sang le même jour »** (`labMarkersForDose` pur —
|
||
🧪↑ avant la dose, 🧪↓ après ; jour calendaire LOCAL, lab à l'heure exacte =
|
||
APRÈS) ; **en-tête « temps sous THS »** (v1.3.1) : carte depuis la 1re prise, jours
|
||
totaux + décomposition mois/jours (`HrtDuration`, mois = 30 j estimés)
|
||
- `LabsScreen` (v1.2.2) : **prise de sang E2 + T en une entrée** (LabDialog create :
|
||
deux sections optionnelles → 1 ou 2 LabResult au même timestamp) ; affichage
|
||
**groupé par timestamp** (`groupLabsForDisplay` : E2 avant T, autres ensuite, tri
|
||
desc) → « E2 306 pg/mL · T 44 ng/dL » côte à côte ; tap → sélecteur E2/T si paire,
|
||
édition unitaire pré-remplie ; suppression = **la prise entière** (confirm nommant
|
||
les valeurs — choix de design : les labs sont prélevés ensemble) ; suggestions
|
||
d'unités pg/mL, ng/mL, ng/dL, ng/L, nmol/L, mIU/L
|
||
- `TreatmentsScreen` : cartes (nom, route, dose, chips ester·modèle / Tmax / ×scale / ⏰,
|
||
badge inactif), FAB → éditeur. **v1.11.0** : ordre d'affichage =
|
||
`treatmentsForDisplay` (pur, tri STABLE — actifs d'abord, inactifs
|
||
regroupés TOUT EN BAS sous un en-tête `inactive_section` qui n'apparaît
|
||
que s'il existe des inactifs) + cartes inactives **atténuées** (alpha
|
||
0,55) — 5 tests `TreatmentsDisplayTest` ; miroir web
|
||
(`js/data/models.js` + `tests/treatments-display.test.js`)
|
||
- `TreatmentEditorScreen` : 12 presets (`PKPresets`, cf `nameRes`) pré-remplissent tout ;
|
||
champs conditionnels (ester+modèle si injection, Bateman sinon) ; carte Calibration
|
||
(scaleFactor + « Calibrer avec les analyses ») ; **section « Posologie » (v1.2 :
|
||
switch « Simuler les doses à venir » + intervalle en jours)** ; carte Rappel (switch +
|
||
TimePicker **centré** + avertissement alarmes exactes) ; switch actif ; save →
|
||
insert/update + schedule/cancel ; delete avec confirmation ; `createdAt` préservé
|
||
- **Logs de diagnostic (v1.3.1, `util/AppLog.kt`)** : journal horodaté
|
||
(buffer circulaire 500 lignes, persisté `filesDir/debug-log.txt`),
|
||
alimenté par l'agenda (permissions, upsert/delete), l'import/export et les
|
||
erreurs ; **export (SAF) et effacement** dans Paramètres — permet le debug
|
||
à distance (l'utilisateur joint les logs à son retour).
|
||
- **Dialog « Nouveautés » (v1.3.0)** : au démarrage, si `BuildConfig.VERSION_NAME`
|
||
est plus récente que la dernière vue (DataStore `changelog_seen_version`),
|
||
un AlertDialog affiche les sections CHANGELOG concernées (extraites par
|
||
`ChangelogHelper.sectionsSince`, asset `changelog.md` synchronisé par la
|
||
tâche `copyChangelog` — gitignoré, source de vérité = docs/CHANGELOG.md) ;
|
||
fermable, ne réapparaît pas avant la prochaine mise à jour.
|
||
- `SettingsScreen` (**v1.9.8 : découpé en cartes thématiques** — 840 lignes
|
||
→ orchestrateur de 377 + 11 fichiers de 33–164 lignes dans le sous-package
|
||
`ui/screens/settings/`, un par carte : `LanguageCard`, `AutoCalibrationCard`,
|
||
`TModelCard`, `AlertThresholdsCard`, `ChartTimezoneCard`, `RemindersCard`,
|
||
`BackupCard`, `AutoBackupCard`, `LogsCard`, `AboutCard` + `SettingsFormat`
|
||
(helpers `formatDoubles`/`folderDisplayName`). L'orchestrateur garde ce qui
|
||
est PARTAGÉ : collecte DataStore, les 4 launchers SAF (leurs callbacks
|
||
écrivent `message`/dialog d'import), dialog de confirmation d'import,
|
||
ordre des cartes. Comportement et rendu IDENTIQUES (déplacement pur avec
|
||
les commentaires historiques — validé par les tests UI §8.ter + smoke
|
||
release §16.ter) ; contenu fonctionnel inchangé : langue
|
||
(Système/Français/English, chips reflétant l'état) ; **carte « Seuils
|
||
d'alerte » (v1.4.2)** : 4 champs optionnels (E2 haut/bas pg/mL,
|
||
T haut/bas ng/mL, vide = pas d'alerte), validation haut > bas (refus + message
|
||
rouge), Save → persiste + **check one-time immédiat** (`AlertNotifier.checkNow`) ;
|
||
**version installée + lien cliquable vers les releases Gitea** (v1.3.0,
|
||
`BuildConfig.VERSION_NAME`, `enableEdgeToEdge`-friendly) ;
|
||
**« Calibration automatique (E2 + T) » — UNE option** (par période d'ester + modèle T,
|
||
v1.2.1, désactivée par défaut) puis réglages T manuels (base/floor/k + bouton
|
||
« Calibrer avec les analyses » ponctuel) ; statut alarmes exactes + bouton d'octroi ;
|
||
Export/Import JSON ; auto-backup (v1.7.0) ; logs ; à propos.
|
||
⚠️ **`ChartScreen` (790 lignes) reste volontairement ENTIER** : sa garde de
|
||
source (`ChartScreenSourceGuardTest`, fix #54) exige que le bloc
|
||
`detectTransformGestures` ET les alias `gesture*` vivent dans CE fichier —
|
||
le découper reviendrait à réécrire la garde et ré-exposer les captures
|
||
figées de pointerInput(Unit). À reconsidérer si une refonte chart a lieu.
|
||
- Composants : `CurveChart` (§11), `DateTimeField` (DatePicker+TimePicker Material3,
|
||
LocalDateTime, **horloge centrée**), `DoseDialog` (create/edit + override d'ester),
|
||
`LabDialog`, `formatDose()` (top-level, dans `DoseDialog.kt`)
|
||
- **Insets edge-to-edge (v1.2.5)** : `enableEdgeToEdge()` dans MainActivity ;
|
||
les insets sont consommés UNE FOIS — TopAppBar M3 (barre de statut, insets
|
||
par défaut) et NavigationBar (barre système) ; les `Scaffold` (racine
|
||
HormoneTrackRoot + imbriqués Doses/Labs) ont `contentWindowInsets = 0` pour
|
||
ne pas cumuler. Avant : fenêtre AppCompat + padding interne TopAppBar =
|
||
double espace vide en haut.
|
||
- Thème M3 custom (`ui/theme/Color.kt` : bleu #4F5BD5, rose #D6589E, labs orange, bandeau
|
||
TransSky/TransPink), dynamic color désactivé ; `MaterialExpressiveTheme` encore
|
||
**internal** dans la ligne material3 pinnée (cf §3) → `MaterialTheme` standard
|
||
- ⚠️ `Card(onClick=…)` et `ExposedDropdownMenuBox` = **API expérimentales M3** → `@OptIn`
|
||
requis sur chaque composable qui les utilise
|
||
|
||
## 11. Graphiques (CurveChart)
|
||
|
||
Canvas pur (aucune lib), **multi-séries** (v1.2) : `ChartSeries(points, e2Style, tStyle?)`
|
||
— le ChartScreen superpose les courbes Estrannaise **et** Transfem Science depuis le
|
||
même traitement (`modelOverride`), styles plein/pointillé par série. Dual axe : E2
|
||
gauche (pg/mL), T droite (ng/mL). Échelle « nice » (`niceCeil` : 1/2/2.5/5/10 × 10ⁿ)
|
||
partagée entre toutes les séries. Grille 4 lignes ; labels Y gauche/droite ; X : pas
|
||
6 h/24 h/5 j selon plage (`SimpleDateFormat` HH'h' / dd/MM). Labs : cercles (E2) et
|
||
carrés (T) orange + valeur, **T convertie en ng/mL** (`convertTToNgMl`) au rendu.
|
||
Ligne verticale « maintenant ». **Pics/creux** (v1.2.3, valeurs v1.2.6) :
|
||
triangles ▲▼ via `PharmacokineticEngine.detectExtrema` **accompagnés de la
|
||
valeur estimée** du pic (au-dessus) ou du creux (en dessous), dans la couleur
|
||
de la série (`drawExtremum`).
|
||
|
||
**Séries « Tracé labs » (v1.5.0/v1.6.0)** : le ChartScreen peut ajouter les
|
||
clés "LAB" (partie ANCRÉE — passe par les labs, rose #C2185B dashed) et
|
||
"LABX" (partie PROLONGÉE — M × ρ_last constant, même rose atténué α 0,55)
|
||
— cf §7.10 / §7.10.bis pour le moteur et les gardes ; les deux clés sont
|
||
SKIPPÉES par la boucle de légende (leçon #63) et ont leurs entrées dédiées
|
||
+ avertissement « simulation » quand LABX est visible.
|
||
|
||
**Zoom (v1.2.9)** : pinch **et** boutons − / + — plage temporelle bornée
|
||
**6 h → 300 j** (`MIN_RANGE_H`/`MAX_RANGE_H`), point focal du pinch maintenu
|
||
fixe dans le temps (formule centroid), échantillonnage **adaptatif**
|
||
(`stepForRange` : 15 min ≤ 12 h, 30 min ≤ 24 h, sinon 1 h — courbes lisses à
|
||
fort zoom) et labels X adaptatifs (1 h / 3 h ajoutés). Le pan ET le zoom
|
||
partagent un seul `detectTransformGestures` (un handler = pas de conflit de
|
||
consommation) ; le zoom/pan restent gérés **par le parent**, le Canvas demeure
|
||
purement déclaratif. **Convention des boutons** (v1.2.10) : « + » = zoom avant
|
||
(fenêtre courte), « − » = zoom arrière — le sens initial était inversé (retour
|
||
utilisateur).
|
||
|
||
Pièges :
|
||
- `DrawScope` implémente `Density` → `X.dp.toPx()` direct ; ne PAS écrire de helper custom
|
||
- Tout label passe par `drawContext.canvas.nativeCanvas` + `android.graphics.Paint`
|
||
- Mélange Double/Float interdit (`1 - i / 4f` et pas `/4.0`)
|
||
- Le panoramique est géré **par le parent** (ChartScreen change `startMs/endMs`), pas par
|
||
le Canvas — le chart reste un composant purement déclaratif
|
||
|
||
## 12. i18n FR/EN
|
||
|
||
- Standard Android : `values/strings.xml` (EN défaut) + `values-fr/strings.xml` (FR).
|
||
L'objet `Strings.kt` custom de la session 1 a été **supprimé**.
|
||
- **Langue par app** : AppCompat 1.7 + `AppCompatDelegate.setApplicationLocales`
|
||
(fonctionne < API 33) ; choix persisté DataStore (`system`/`fr`/`en`), appliqué au
|
||
démarrage. Thème app = `Theme.AppCompat.DayNight.NoActionBar` (requis par AppCompat).
|
||
- Notifs localisées via `context.getString(R.string.*)`
|
||
- ⚠️ **Toute nouvelle string = les DEUX fichiers** (une référence manquante = erreur de
|
||
compilation `Unresolved reference 'active'` — déjà arrivé)
|
||
|
||
## 13. Sauvegarde JSON
|
||
|
||
`data/backup/BackupManager.kt` :
|
||
- `BackupData{version=2, exportedAt, treatments[], doseLogs[], labResults[], tConfig,
|
||
settings?}` → Gson — **v2 (v1.4.2)** : champ optionnel `settings: UserSettings`
|
||
(champs PLATS : language, autoCalibrate, alertE2High/Low pg/mL, alertTHigh/Low
|
||
ng/mL). **Rétrocompat** : les backups v1 (sans settings) restent parsables
|
||
(null) et importables ; l'import ne vérifie pas strictement la version.
|
||
`changelog_seen_version` volontairement EXCLU (pas une donnée utile à
|
||
restaurer). ⚠️ R8 : UserSettings est lue par réflexion Gson → `-keep`
|
||
explicite dans proguard-rules.pro (sinon objets vides en release seule).
|
||
- **Les IDs Room sont conservés** dans l'export et réinsérés tels quels → les FK
|
||
dose→traitement restent valides
|
||
- Import : **mode ÉCRASEMENT (v1.2.6)** — les données actuelles sont effacées
|
||
AVANT l'insertion (`deleteAllDoseLogs` → `deleteAllLabResults` →
|
||
`deleteAllTreatments`, ordre enfants → parents pour la FK CASCADE), puis
|
||
restauration avec les IDs du backup conservés (FK valides). Avant (v1.2.5) :
|
||
insertion en « ajout » → conflit d'ID dès que l'app contenait des données →
|
||
l'import échouait (bug remonté). Après un import : le `tConfig` du backup est
|
||
restauré dans DataStore, **les paramètres utilisateur sont restaurés**
|
||
(v1.4.2 : auto-calibration, seuils d'alerte, langue — la LANGUE est
|
||
appliquée sur le thread MAIN car `setApplicationLocales` recrée
|
||
l'activité) et les rappels sont reprogrammés (`rescheduleAll`). Le dialog prévient que TOUT sera
|
||
remplacé (bouton « Effacer & restaurer »)
|
||
- Transport : SAF (`CreateDocument("application/json")` / `OpenDocument`), écriture
|
||
`openOutputStream(uri, "wt")` ; ⚠️ pas de `return` dans un expression body `= try{}`
|
||
|
||
### 13.bis Sauvegarde automatique quotidienne (v1.7.0) — `data/backup/AutoBackupWorker.kt`
|
||
|
||
Opt-in (désactivée par défaut), en PLUS de l'export manuel (inchangé).
|
||
Vue d'ensemble : **dossier choisi UNE FOIS → WorkManager écrit un backup
|
||
JSON complet chaque jour, garde les N copies les plus récentes.**
|
||
|
||
- **Réglages (DataStore, [AppSettings])** : `autoBackupEnabled` (bool),
|
||
`autoBackupTreeUri` (tree URI SAF persisté), `autoBackupKeep` (1..30,
|
||
défaut 7, borné au SET), `autoBackupLastRun` (résultat + horodatage,
|
||
affiché dans Paramètres).
|
||
- **Dossier SAF (sans permission de stockage)** :
|
||
`ACTION_OPEN_DOCUMENT_TREE` → `takePersistableUriPermission(READ|WRITE)`
|
||
→ la permission SURVIT au reboot ; le worker écrit ensuite via
|
||
[BackupManager.writeAutoBackup] : tree URI → document URI du dossier
|
||
(DocumentsContract, SANS la lib androidx.documentfile) →
|
||
`createDocument` d'un NOUVEAU fichier horodaté
|
||
(`hormonetrack-auto-YYYYMMDD-HHmm.json` — ExportFileNames, signature
|
||
LocalDateTime obligatoire, règle anti-#48) → écriture via `writeBackup`
|
||
TEL QUEL (leçon #45 : l'IO éprouvée de l'export manuel) → purge.
|
||
- **Rétention ([AutoBackupRetention], PUR et testé)** : décision
|
||
« quels noms supprimer » séparée de l'IO ; seuls les auto-backups
|
||
RECONNAISSABLES participent (parse de l'horodatage du NOM — la mtime
|
||
SAF n'est pas fiable sur tous les providers) ; exports manuels, logs et
|
||
fichiers étrangers du dossier JAMAIS touchés ; noms malformés jamais
|
||
supprimés ; `keep` clampé ≥ 1. Pourquoi un fichier PAR RUN au lieu
|
||
d'écraser un fichier fixe : une écriture interrompue (quota cloud,
|
||
réseau) ne doit jamais détruire la dernière bonne copie.
|
||
- **Planification ([AutoBackupScheduler])** : `schedulePeriodic` au
|
||
démarrage (HormoneTrackApp.onCreate, `ExistingPeriodicWorkPolicy.KEEP`
|
||
— ne repousse pas le prochain run) ; périodique 24 h (survivre au
|
||
reboot, Doze géré par WorkManager — pas d'alarme exacte nécessaire) ;
|
||
`runNow` à l'ACTIVATION (feedback immédiat + validation émulateur
|
||
triviale). Le worker lit ses options dans DataStore à CHAQUE exécution :
|
||
changer dossier/copies ne requiert aucun re-enqueue ; option désactivée
|
||
→ no-op immédiat (pattern AlertWorker sans seuils).
|
||
- **Contenu** : [BackupManager.exportJson] complet (format v2, paramètres
|
||
inclus) → un auto-backup est IMPORTABLE tel quel (même dialog que
|
||
l'export manuel).
|
||
- **R8 (leçon #64)** : `-keep class ...AutoBackupWorker { *; }` — le
|
||
worker est instancié par WorkManager via réflexion.
|
||
- **Écran Paramètres** : switch (ON sans dossier → ouvre le picker, la
|
||
sélection active + premier run), nom du dossier lisible
|
||
(`folderDisplayName` : « primary:Download » → « Download »), copies
|
||
conservées (Save borné), statut dernier run (OK/ÉCHEC + date).
|
||
- 9 nouveaux tests (200 verts).
|
||
|
||
## 14. Bugs corrigés
|
||
|
||
Historique complet — **à ne pas réintroduire** (utile pour diff/revert) :
|
||
|
||
**Session 1 → 2 (avant tout build) :**
|
||
1. `settings.gradle.kts` : `dependencyResolution` (inexistant) → `dependencyResolutionManagement`
|
||
2. `BootReceiver` : `runBlocking { flow.collect {…} }` → blocage infini → one-shot + goAsync
|
||
3. `AlarmScheduler.cancel` : Intent sans l'action → annulation inopérante → fabrique unique
|
||
4. `PKProfileStore` : parsait la racine JSON → crash → lecture de `profiles`
|
||
|
||
**Session build (détectés à la compilation) :**
|
||
5. `kotlin.math.ln2` **n'existe pas** (hallucination) → `ln(2.0)` ; cascade d'erreurs sur
|
||
les lignes suivantes du même fichier (opérateurs sur types error)
|
||
6. Mélange **Double/Float** interdit en Kotlin : `mg * bioavailabilityFraction` (Float),
|
||
`30.0 * t½` (Float), `Float×exp()`… → `.toDouble()` partout
|
||
7. `BackupManager.writeBackup` : `return` dans expression body `= try{}` → block body
|
||
8. `DateTimeField` : `spacedBy(8f/2f*8)` (Float sans unité) → `8.dp` + import `dp` manquant
|
||
9. `DateTimeField` : extension `fun LocalDate.Companion.ofEpochMs` (java.time n'a pas de
|
||
Companion) → supprimée ; imports nettoyés
|
||
10. `CurveChart` : helper `dpToPx()` custom cassé → `dp.toPx()` de `DrawScope`
|
||
11. `CurveChart` : labels Y en Double (`i / 4.0`) → `i / 4f`
|
||
12. `LabsScreen` / `TreatmentsScreen` : imports `dp` / `fillMaxWidth` manquants
|
||
13. `TreatmentEditorScreen` : `R.string.active` inexistante → string ajoutée EN+FR
|
||
14. `TreatmentCard` : `Card(onClick=…)` sans `@OptIn(ExperimentalMaterial3Api::class)`
|
||
15. Typo `Locale.getDefault` sans parenthèses (SimpleDateFormat)
|
||
16. `SettingsScreen` : chips de langue codées en dur → état depuis DataStore
|
||
17. `ReminderManager` : constantes d'action mortes → implémentées (`DoseActionReceiver`)
|
||
18. `TreatmentEditorScreen` : `createdAt` écrasé à l'édition → préservé
|
||
|
||
**Session tests (bugs SÉMANTIQUES trouvés par les tests unitaires) :**
|
||
19. **`computeKa` : bisection inversée** — `if (eq > 0) hi = mid else lo = mid` convergeait
|
||
vers ka énorme (pic à ~0 h au lieu de Tmax) ; bug présent depuis la session 1, jamais
|
||
testé. → `if (eq > 0) lo = mid else hi = mid` (eq décroît en mid ; eq>0 ⇒ racine au-dessus)
|
||
20. **Plancher d'affichage des profils** : l'ODS arrondit à 2 décimales → queues à 0,01/0,00
|
||
; extrapoler depuis la fin de table donnait 0 à vie (ou une constante plate). →
|
||
extrapolation depuis le dernier point ≥ 1 % du pic avec pente sur 48 h
|
||
21. **Mapping silencieux du modèle** : `profileKey` mappe tout modèle ≠ "TFS" sur "ese"
|
||
→ `sample("EV","XXX")` renvoyait EV_ese. → validation stricte dans `sample`
|
||
|
||
**Session v1.1.0 (remontées par l'utilisatrice, reproduites en test) :**
|
||
22. **Casse des clés de profils** — LE bug « les graphiques ne se génèrent pas » :
|
||
l'asset contient `"EEn_ese"`/`"EEn_tfs"` (casing biologique du `.ods`) mais
|
||
`Esters.EEN = "EEN"` → lookup exact null → `sample()=0` pour tout traitement EEn
|
||
(courbe E2 plate à 0, T plate à la base). EV/EU marchaient (casse identique) et les
|
||
tests profils utilisaient la casse "EEn" — le trou passait entre les deux.
|
||
→ **lookup insensible à la casse** (`PKProfileStore.lookup()`), régression épinglée
|
||
sur les données réelles (`RegressionUserCaseTest`).
|
||
23. **Unités T non converties** : labs saisis en ng/dL (32/45) → axe T du chart à
|
||
×100 (courbe T invisible) et calibration T fausse. →
|
||
`PharmacokineticEngine.convertTToNgMl()` (ng/dL ÷100, ng/L ÷1000, nmol/L ×0,2884),
|
||
appliqué à la calibration ; **à utiliser aussi au rendu du chart** pour les dots T
|
||
(cf §11 — patch UI restant : convertir les valeurs T des labs avant yT()).
|
||
24. **Pas d'édition des doses** : suppression+recréation obligatoire. → `DoseDialog`
|
||
create/edit (préfill, changement de traitement, date/heure, notes, **override
|
||
d'ester par injection**), appelé depuis DosesScreen (tap sur la ligne) ;
|
||
`LogDoseDialog` supprimé (attention : `formatDose` vivait dedans → déplacée
|
||
top-level dans `DoseDialog.kt`).
|
||
|
||
**Session v1.2.1/v1.2.2 (remontées utilisateur) :**
|
||
25. **Calibration mélangeant les périodes d'ester** — le fond du « 250–375 » :
|
||
un facteur unique par traitement comparait des labs valerate à des prédictions
|
||
enanthate → ratios aberrants → courbes gonflées. → calibration **par période**
|
||
(`computeEsterScaleFactors` + `scalePerEster`, cf §7.6) ; vérifié que l'état
|
||
d'équilibre EEn (accumulation ×2) est CORRECT — le « 375 » = calcul non calibré.
|
||
26. **Lab T en unité aberrante (« 38 pg/mL », faute de frappe)** : renvoyé brut il
|
||
écrasait l'axe T et rendait la courbe T invisible. → `convertTToNgMl` avec
|
||
branche défensive pg/mL ÷1000 (et µg/L).
|
||
27. **Pas d'édition des labs** → LabDialog create/edit + tap sur la ligne ;
|
||
formatLabValue (préserve les décimales, contrairement à formatDose).
|
||
|
||
**Session v1.2.3/push Gitea :**
|
||
28. **Commentaires Kotlin imbriqués** : `/**TFS**` dans un KDoc ouvre un bloc
|
||
`/**` JAMAIS fermé (Kotlin les imbrique !) → « Unclosed comment » → NE PAS
|
||
mettre `/**` dans un texte de doc ; écrire « EEn + TFS ».
|
||
29. **XML de test périmés après échec de compilation** : quand compile échoue,
|
||
les anciens XML test-results restent → on « corrige » des échecs fantômes.
|
||
Toujours vérifier que la COMPILATION passe (grep `^e:` du log) avant
|
||
d'analyser les résultats de tests.
|
||
30. **filter-branch non idempotent** : le scrub (45.0→44.0) a corrompu une
|
||
constante d'un test PUR écrit après coup (assert 0.45 vs 44/100). Règle :
|
||
les remplacements d'anonymisation doivent être idempotents (les valeurs de
|
||
remplacement ne re-matchent jamais les motifs) et les tests purs doivent
|
||
utiliser des constantes hors des motifs de scrub.
|
||
31. **Release script : préfixe « v »** — le tag git = `v1.2.3`, la CHANGELOG
|
||
titre `[1.2.3]` : l'extraction cherchait `[vX.Y.Z]` → fallback silencieux.
|
||
→ `scripts/gitea-release.py` (corps = section CHANGELOG, APK attaché).
|
||
|
||
34. **Import JSON impossible si l'app contient déjà des données** (v1.2.6) :
|
||
l'insertion en « ajout » avec les IDs du backup entrait en conflit
|
||
d'unicité dès la moindre donnée existante → import échouait. → mode
|
||
ÉCRASEMENT (effacement ordonné enfants→parents puis restauration) +
|
||
restauration du tConfig + reprogrammation des rappels.
|
||
37. **Upload d'asset Gitea instable** (v1.2.10) : le `?name=` du POST et le
|
||
PATCH de renommage peuvent être ignorés (asset au nom générique, APK
|
||
release disparu de la release). → `ensure_asset` dans le script :
|
||
upload + vérification nom/taille + retry PATCH + échec bruyant.
|
||
42. **Dialog « Nouveautés » à chaque réouverture** (v1.3.2, remontée) :
|
||
la version vue était mémorisée APRÈS l'affichage — si l'app est fermée
|
||
pendant que le dialog est ouvert, DataStore n'avait pas encore flushé
|
||
→ même changelog ressortait. → mémoriser la version vue AVANT
|
||
d'afficher (l'affichage est purement visuel, la mémoire est déjà
|
||
faite). Titre = BuildConfig.VERSION_NAME (version de L'APP) : si
|
||
l'APK installé est v1.3.0, le titre montre 1.3.0 même si le contenu
|
||
parle d'une version plus récente — installer le dernier APK.
|
||
40. **Toggle agenda toujours inopérant** (v1.3.2, remontée) : le FIX v1.3.1
|
||
(callback async) était correct mais insuffisant : (a) le switch
|
||
s'activait même SANS Posologie (l'événement RRULE aurait un INTERVAL
|
||
invalide) → désormais refusé avec feedback rouge ; (b) au save, la
|
||
permission était vérifiée via le flag `remembered` (peut être faux
|
||
après recomposition) → vérification RÉELLE via ContextCompat ;
|
||
(c) AppLog journalise chaque étape (permission demandée/accordée,
|
||
upsert, delete) pour le debug à distance.
|
||
39. **Toggle de l'agenda inopérant** (v1.3.1, remontée) : le résultat de la
|
||
permission arrive ASYNCHRONE — le test synchrone juste après `launch()`
|
||
était toujours faux → le switch rebondissait sans s'activer, sans
|
||
feedback. → le CALLBACK active le switch ; journalisation AppLog ;
|
||
leçon : ne jamais lire un état de permission juste après launch().
|
||
41. **Export des logs plantait l'app** (v1.3.2, remontée) : le launcher
|
||
CreateDocument avec écriture inline dans une coroutine IO non couverte
|
||
par le même pattern que l'export JSON. → réutilise BackupManager
|
||
(openOutputStream "wt") + journalisation AppLog à chaque étape.
|
||
Leçon : quand un pattern d'IO marche (export JSON), le réutiliser
|
||
tel quel plutôt que d'en écrire un nouveau.
|
||
38. **Piège lexicographique de tags/versions** (v1.3.0, publication
|
||
farewell) : `tag >= "v1.2.5"` en comparaison de CHAÎNES fait `v1.2.10 <
|
||
v1.2.5` ("1" < "5") → la boucle de publication a sauté v1.2.10. →
|
||
toujours comparer les versions STRUCTURÉEMENT (tuple numérique, cf
|
||
ChangelogHelper.isVersionNewer) ; même famille que le bug #22 (casse
|
||
EEn) : identifiants « presque pareils ». (Numéro corrigé en v1.3.3 :
|
||
l'entrée était dupliquée sous « #41 » alors que §2/§16 référencent #38.)
|
||
43. **Course entre uploads rapprochés** (v1.2.6→v1.3.2, confirmé 4 fois) :
|
||
publier les 2 APK par DEUX INVOCATIONS rapprochées de gitea-release.py
|
||
→ le second upload écrase le premier (APK release disparu de la
|
||
release). → `scripts/publish-release.py` : UNE invocation fait purge +
|
||
upload des 2 APK + vérification PAR TÉLÉCHARGEMENT de chacun (§16.bis).
|
||
36. **Sens des boutons de zoom inversé** (v1.2.10, retour utilisateur) :
|
||
le « + » dézoomait (fenêtre plus longue) et le « − » zoomait — contre
|
||
la convention carte. → « + » = zoom avant (fenêtre courte), « − » =
|
||
zoom arrière. Le pinch (écarter = zoom avant) était déjà correct.
|
||
35. **Prévision cassée après un oubli d'injection** (v1.2.7) : le premier
|
||
créneau projeté tombait dans le passé (dernière + intervalle = jour
|
||
manqué) → faux pic dans l'historique + rythme décalé. → les créneaux
|
||
passés sont sautés (`while (t <= nowMs) t += intervalMs`), la prévision
|
||
démarre au premier créneau futur ; un retard décale toute la prévision
|
||
(comportement voulu, testé). NB : la doc §7.3b affirmait « strictement
|
||
après nowMs » alors que le code incluait les créneaux passés —
|
||
mismatch doc-code détecté par l'utilisatrice.
|
||
33. **Double insets edge-to-edge** (v1.2.5) : espace vide en haut de l'écran —
|
||
la fenêtre AppCompat poussait le contenu sous la barre de statut ET les
|
||
TopAppBar M3 rajoutaient leur padding interne. → `enableEdgeToEdge()` +
|
||
`contentWindowInsets = 0` sur les Scaffold, insets consommés une seule fois.
|
||
32. **`isActive` traité comme filtre de données** (v1.2.4) : passer un
|
||
traitement à inactif effaçait sa simulation du graphique (écrans
|
||
collectaient `activeTreatments`) tout en permettant d'y loger des doses.
|
||
→ sémantique documentée §6.bis : l'inactivation ne touche que la SAISIE
|
||
(chips, dropdown création, rappels) ; simulation et calibration reçoivent
|
||
TOUS les traitements. Épinglé par la régression n°3.
|
||
|
||
**Session v1.3.3 (audit de reprise de maintenance — session IA démarrant sans
|
||
aucun contexte, dans l'esprit de cette doc) :**
|
||
|
||
44. **Permissions agenda absentes du manifest** (v1.3.0 → v1.3.2) : la
|
||
fonctionnalité « événement d'agenda récurrent » ne pouvait JAMAIS
|
||
fonctionner — `WRITE_CALENDAR`/`READ_CALENDAR` n'étaient **pas déclarées
|
||
dans AndroidManifest.xml** (la demande limitée à l'exécution est refusée
|
||
d'office sans déclaration, et `CalendarEvents.ensureCalendar/upsert`
|
||
lèvent SecurityException). La doc §9 les listait pourtant depuis v1.3.0
|
||
→ leçon : « documenté » ≠ « implémenté » ; toute affirmation « le
|
||
manifest contient X » doit être vérifiée dans le XML réel (30 s de
|
||
`grep uses-permission`). Les fixes v1.3.1/v1.3.2 du « toggle agenda »
|
||
réparaient la logique UI mais pas la cause racine. FIX : déclaration +
|
||
commentaire dans le manifest.
|
||
45. **Export des logs plantait TOUJOURS l'app** (le « fix » v1.3.2, #41
|
||
plus haut, était insuffisant) : l'IO restait réimplémentée inline
|
||
(leçon du #41 : réutiliser BackupManager — PAS appliquée) et 3 appels
|
||
`AppLog.log` du callback étaient HORS try/catch — or une exception non
|
||
interceptée dans une coroutine à scope racine (`CoroutineScope
|
||
(Dispatchers.IO)`) remonte au handler de la thread = crash du process.
|
||
FIX : (a) l'export réutilise `BackupManager.writeBackup` TEL QUEL —
|
||
le même code path que l'export JSON qui marche sur le Pixel 9 /e/OS ;
|
||
(b) tout le callback est gardé ; (c) `AppLog.log` est fail-safe
|
||
(l'IO fichier est avalée dans le wrapper — une erreur disque ne doit
|
||
jamais tuer l'app pour une ligne de journal) ; (d) feedback visible
|
||
succès/échec + `logLineCount` rafraîchi (sur Main).
|
||
46. **Bump de version jamais commité** (v1.3.0 → v1.3.2) : les tags
|
||
v1.3.0/v1.3.1/v1.3.2 contenaient TOUS `versionCode = 14`
|
||
/ `versionName = "1.3.0"` → `BuildConfig.VERSION_NAME` était faux dans
|
||
les APK publiés (Paramètres, titre du dialog « Nouveautés »,
|
||
comparaison `isVersionNewer`). La checklist §16 étape 1 existait mais
|
||
n'a jamais été appliquée. FIX : versionCode 17 / « 1.3.3 » commité
|
||
AVANT le tag ; le bump fait partie du commit de release (jamais un
|
||
état local non commité au moment du build).
|
||
|
||
**Session v1.3.4 (deux crashs remontés → reproduits sur émulateur) :**
|
||
|
||
47. **L'écran Doses crashait l'app** (`MissingFormatArgumentException:
|
||
Format specifier '%3$d'`) : la string `hrt_duration` a TROIS placeholders
|
||
(`%1$d mois, %2$d jours, %3$d total`) mais
|
||
`stringResource(R.string.hrt_duration, months, days)` ne passait que DEUX
|
||
arguments. Crash dès que le fragment `totalDays > 0` s'affiche (donc pour
|
||
TOUTE donnée antérieure à aujourd'hui) — né en v1.3.1 (string + appel dans
|
||
le même commit fa5d2df, jamais testés ensemble) et passé à travers
|
||
v1.3.1→v1.3.3.
|
||
48. **Le bouton « Exporter » des logs crashait l'app** — LE bug récurrent des
|
||
v1.3.1→v1.3.3 : le nom de fichier était construit INLINE via
|
||
`LocalDate.now().format(ofPattern("yyyyMMdd-HHmm"))`. Un `LocalDate` n'a
|
||
PAS de champ horaire → `UnsupportedTemporalTypeException:
|
||
Unsupported field: HourOfDay` levée dans le onClick (thread principal,
|
||
synchrone, touch dispatch) → crash au tap, avant même le sélecteur SAF
|
||
(stack obtenue sur émulateur : LTSI + frames R8 obfusquées). L'export
|
||
JSON marchait car `yyyyMMdd` est un pattern valide pour LocalDate.
|
||
Zones d'ombre emboîtées : v1.3.2 et v1.3.3 « corrigent » l'IO (cf #41/#45)
|
||
sans voir cette ligne. FIX : `util/ExportFileNames.kt` (pur, testé,
|
||
documenté — couplage type↔pattern centralisé) ; `ExportFileNamesTest` épinglant
|
||
les deux helpers (aurait attrapé le bug le jour même). Leçon : la
|
||
génération de nom de fichier ne doit JAMAIS vivre inline dans un onClick.
|
||
49. **StringFormatMatches** (capture du lint, v1.3.4) : la notification de
|
||
rappel passait un Double à `%s`. Fix `dose.toString()` (l'affichage ne
|
||
change pas). L'intérêt des règles lint est confirmé PAR CETTE SESSION :
|
||
`./gradlew lint` aurait signalé #47 dès v1.3.1 → le lint fait désormais
|
||
partie de la vérification avant release (les checks Compose 1.12+
|
||
théoriques `NonObservableLocale`/`LocalContextGetResourceValueCall` sont
|
||
tombés en warning via `app/lint.xml`, cf §19).
|
||
|
||
**Session v1.3.5 (diagnostic à distance via les logs exportés — la boucle par
|
||
l'exemple) :**
|
||
|
||
50. **Création du calendrier d'agenda impossible** (`IllegalArgumentException:
|
||
Sync adapters must specify an account and account type`, remontée par les
|
||
AppLog exportés du Pixel 9 v1.3.4) : `CalendarEvents.ensureCalendar`
|
||
construisait l'URI calendars avec `CALLER_IS_SYNCADAPTER=true` SANS
|
||
`ACCOUNT_NAME`+`ACCOUNT_TYPE` — requis par le CalendarProvider pour TOUTE
|
||
opération sync-adapter (requête ET insertion). De plus :
|
||
- les mêmes logs CONFIRMAIENT au passage les fixes précédents : export
|
||
logs `ok=true` (#48 ✓ depuis v1.3.4 au téléphone), permission agenda
|
||
accordée (manifest v1.3.3 ✓) — AppLog fait son job de diagnostic à
|
||
distance ;
|
||
- le log « événement agenda supprimé » était imprimé aussi quand le
|
||
switch était inéligible (posologie absente, permission absente,
|
||
switch off) → trois messages distincts désormais (créé-mis à jour /
|
||
supprimé / non activé) ;
|
||
- contexte : les « événement agenda supprimé » répétés dans les logs =
|
||
des saves où le switch ne remplissait pas les conditions d'activation
|
||
(l'utilisatrice avait retourné le switch après les échecs) — les trois
|
||
messages distincts rendront les prochains logs univoques.
|
||
FIX : append ACCOUNT_NAME/ACCOUNT_TYPE sur l'URI ; le pattern complet :
|
||
`syncadapter=true` + account + account_type sur les URIs de CRÉATION
|
||
(calendriers) ; l'insertion d'EVENTS l'avait déjà (upsertEvent). Validé
|
||
émulateur : `content query --uri content://com.android.calendar/calendars`
|
||
montre le calendrier, events avec RRULE corrects.
|
||
50.bis. **Delete d'événement silencieusement manqué** (cité au #50) : le même
|
||
défaut (CALLER_IS_SYNCADAPTER sans account) sur l'URI de DELETE de
|
||
`deleteEvent` — l'exception est avalée par le catch, le log dit
|
||
« supprimé » mais l'event reste dans le provider. FIX : mêmes params
|
||
sur l'URI. Règle : TOUT URI avec CALLER_IS_SYNCADAPTER doit embarquer
|
||
account+account_type — pas seulement les insertions.
|
||
51. **Événement orphelin après re-save** (découvert en validant #50 sur
|
||
émulateur — le provider montrait un event que la base ne référençait
|
||
plus) : `buildTreatment()` reconstruisait le Treatment SANS le champ
|
||
`calendarEventId` → null à chaque save → l'event existant devenait
|
||
orphelin dès la deuxième sauvegarde, plus jamais désactivable. FIX :
|
||
`loadedCalendarEventId` chargé dans le LaunchedEffect + renvoyé par
|
||
buildTreatment(). Leçon : la reconstruction complete d'une entité pour
|
||
un save doit repartir des champs NON ÉDITABLES (voir `loadedCreatedAt`,
|
||
déjà protégé, et désormais `loadedCalendarEventId`).
|
||
|
||
**Session v1.4.0 (modèle TFS sur la méta-analyse + fix #52) :**
|
||
|
||
62. **Pan mort sur la vue 24 h** (v1.4.10, remontée par test utilisateur
|
||
Pixel 9 : « le glisser horizontal ne marche pas sur 24 h, uniquement
|
||
au-dessus ») : le pan incrémental tronquait CHAQUE delta en heures
|
||
entières (`(pan.x / largeur * plage).toLong()`) — à 24 h, le delta
|
||
d'un doigt réel (~30 px ≈ 0,67 h) tombait à 0 à CHAQUE événement →
|
||
le pan ne bougeait jamais sur 24 h (sur 7 j : le même delta = 4,7 h).
|
||
FIX : cumul fractionnaire (`panDeltaHours` — le résiduel < 1 h est
|
||
conservé d'un événement au suivant, pur et testé). Leçon : un cast en
|
||
entiers par delta incrémental tue les mouvements fins — cumuler le
|
||
résiduel fractionnaire. ⚠️ Le pan n'est PAS testable par adb sur
|
||
l'émulateur headless (les `input swipe` ne délivrent pas les
|
||
mouvements intermédiaires au chart) — le test humain sur Pixel 9
|
||
reste la référence pour les gestes (§16.ter).
|
||
|
||
61. **Calibration tombant à ×2,21 (auto ET manuelle)** (v1.4.9, remontée
|
||
« le bouton Calibrer avec les analyses du traitement EEn WHSAH me met
|
||
toujours à ×2,21 ») : reproduction par test sur données réelles —
|
||
l'export contenait une dose EEn de test de JANVIER (10 mg) ; les labs
|
||
de janvier-mars tombaient 15–76 j après, avec une prédiction WHSAH
|
||
résiduelle (184 → 1 pg/mL) → ratios aberrants (×3,3 → ×391) → la
|
||
MÉDIANE des ratios tombait à ×2,21, dans les DEUX pipelines (manuel ET
|
||
auto — même défaut de frontière). FIX : garde de significativité
|
||
`labIsSignificant` (prédiction ≥ 15 % du max observé, labs par
|
||
timestamp croissant) appliquée aux 3 pipelines ; facteur WHSAH réel =
|
||
0,55 (épinglé). Leçon : une garde de significativité RELATIVE (le lab
|
||
ne calibre que ce que le modèle « fait » encore) plutôt qu'une
|
||
fenêtre temporelle fixe — les tests existants (labs à 2-7 j, t½ courte)
|
||
auraient été cassés par une fenêtre en jours.
|
||
|
||
59. **« EEN Estrannaise » pour un traitement sous WHSAH** (v1.4.7,
|
||
remontée) : le chip de `TreatmentsScreen` affichait le modèle via un
|
||
ternaire à 2 branches (`if (pkModel == TFS) model_tfs else model_ese`)
|
||
→ WHSAH tombait dans le `else`. Fix : helper centralisé
|
||
`modelLabelRes(pkModel)` (data/model/Treatment.kt) — UNIQUE source du
|
||
label. Leçon : quand un 3ᵉ choix est ajouté à un champ, GREPPER tous
|
||
les points d'affichage existants (l'éditeur et le chart avaient été
|
||
fixés en v1.4.6, la carte de traitement avait été ratée) — l'audit de
|
||
`grep -rn "model_tfs else"` est passé à côté du pattern retourné
|
||
multi-ligne ; le vrai filet : chercher les USAGES du champ
|
||
(`tr.pkModel`) et vérifier chaque site.
|
||
|
||
57. **Écran Graphiques : rangée des toggles coupée sous le pli** (v1.4.6,
|
||
découvert en validant WHSAH sur émulateur : le chip était absent du
|
||
dump UI) : le Column du ChartScreen ne défilait pas — les toggles de
|
||
modèles (et leur LÉGENDE) étaient invisibles sur un téléphone standard.
|
||
FIX : écran à défilement vertical. Leçon : un ajout de rangée dans un
|
||
écran non-scrollable coupe ce qui suit — tester tout nouvel élément UI
|
||
par dump émulateur, pas seulement le code.
|
||
|
||
55. **« La prévision simule le 13 au lieu du 12 »** (v1.4.5, remontée) :
|
||
diagnostic sur les données réelles (régression #4) : la GÉNÉRATION est
|
||
exacte (créneau = dernière dose LOGUÉE + intervalle en ms exact) —
|
||
l'illusion venait de trois défauts d'AFFICHAGE : aucun marqueur des
|
||
doses simulées, labels X alignés sur minuit UTC (= 02:00 FR), courbe
|
||
encore descendante au créneau (physiologie EEn). FIX : marqueurs
|
||
(prévisionnels pointillés + réels discrets), `xLabelTicks` à minuit
|
||
LOCAL dans le fuseau CHOISI (Paramètres, null = téléphone).
|
||
⚠️ Subtilité : une dose loguée le LENDENDE de l'injection réelle décale
|
||
le rythme d'un jour par cycle — l'app suit les LOGS ; recadrer = éditer
|
||
la dose à son instant réel.
|
||
56. **Boucle de recomposition saturant le main thread** (v1.4.5, découvert
|
||
en validant #55 : les chips ne répondaient plus, « Skipped 52 frames »
|
||
en continu) : `nowMs = System.currentTimeMillis()` relu frais à CHAQUE
|
||
recomposition → endMs dérive → les keys du producer (endMs) changent à
|
||
chaque frame → re-calcul perpétuel. FIX : nowMs mémoïsé sur le tick
|
||
minute (`remember(tick)`) ; le calcul des créneaux prévisionnels
|
||
mémoïsé aussi. Leçon : JAMAIS de `System.currentTimeMillis()` nu dans
|
||
un corps de composable dont il dérive des keys de producer — le mémoïser
|
||
sur un tick.
|
||
|
||
54. **Prévision inaccessible : captures figées dans `pointerInput(Unit)`**
|
||
(v1.4.4, remontée « le toggle activé ne génère pas les prévisions, je ne
|
||
peux même pas dragger vers la gauche ») : la closure du gestionnaire de
|
||
gestes est créée UNE FOIS (`pointerInput(Unit)`) ; les `val` calculées
|
||
de la composition (`futurePanHorizon`, `maxPanHours`, `forecastExtensionH`)
|
||
y étaient FIGÉES à leur valeur initiale (0, chip désactivé) → le clamp
|
||
interdisait le pan vers le futur POUR TOUJOURS, même après activation du
|
||
chip. Fix : `rememberUpdatedState` (les `MutableState` n'ont pas le
|
||
problème : c'est la fermeture `by` qui lit à jour). + cause secondaire :
|
||
`generateForecastDoses(toMs = endMs = now)` ne générait rien → extension
|
||
contrôlée à l'activation (cf §10). **Garde de source** :
|
||
`ChartScreenSourceGuardTest` (extraire la closure et interdire les
|
||
lectures des vals brutes). Leçon : dans `pointerInput(Unit)`, TOUTE val
|
||
calculée lue par la closure doit passer par `rememberUpdatedState` —
|
||
seul `pointerInput(key)` re-keyé relance la closure (et perd les gestes
|
||
en cours).
|
||
|
||
53. **Delta « vs il y a 6 h » = en réalité 24 h** (v1.4.3, remontée) :
|
||
`NowLevelCard` prenait `curve.firstOrNull { écart ≥ 6 h }` sur une
|
||
fenêtre de 24 h — le premier point satisfait la condition
|
||
immédiatement → comparaison à ~24 h. Le lendemain d'une injection EEn
|
||
(plateau), ce delta 24 h peut être légèrement négatif → fausse
|
||
impression que « l'injection ne fait rien ». FIX : `pointHoursBefore`
|
||
(point au plus petit écart ≥ 6 h, testé). Leçon : un `firstOrNull` sur
|
||
une condition de distance trouve l'élément le PLUS LOIN, jamais le plus
|
||
proche — écrire l'intention (« le plus proche de N ») explicitement.
|
||
|
||
52. **Rappels quotidiens même hors jour d'injection** (remontée utilisateur :
|
||
« je m'injecte tous les samedis à 18 h → je dois recevoir le rappel
|
||
uniquement le samedi à 18 h ») : `nextReminderFireMs` ignorait doses et
|
||
Posologie → l'alarme se ré-armait chaque jour à HH:mm. FIX : le prochain
|
||
déclenchement suit la GRILLE (dernière dose + k × intervalle, créneaux
|
||
passés sautés — même sémantique que la prévision) à l'heure de rappel
|
||
choisie ; fallback quotidien sans Posologie/sans doses ; **et** la
|
||
chaîne one-shot est recousue après notif ET après « Pris » (avant :
|
||
seulement au boot/save). Leçon : toute logique de scheduling doit vivre
|
||
au MOTEUR (testable JVM), pas dans les composants Android.
|
||
|
||
63. **Légende du graphique dupliquée + chip coupé hors fenêtre** (v1.5.0,
|
||
constat émulateur §16.ter) : l'ajout de la série « Tracé labs »
|
||
(clé `"LAB"` dans la liste `curves`) tombait dans le `else` de la
|
||
boucle de légende → la légende « E2/T Transfem Science » s'imprimait
|
||
une 2ᵉ fois ; et le 4ᵉ chip de la rangée modèles sortait de la
|
||
fenêtre sur écran 1080 px (rangée à largeur fixe, absent du dump
|
||
uiautomator — même famille que #57 : un ajout de rangée doit être
|
||
testé par dump, pas seulement par le code). FIX : (a) branche
|
||
explicite `"LAB" -> {}` dans la boucle de légende (légende dédiée
|
||
plus bas) ; (b) `horizontalScroll(rememberScrollState())` CONFINÉ à
|
||
la RANGÉE de chips (le pan/pinch du graphique reste sur le Card — le
|
||
fix #62 visait un scroll VERTICAL parent ; ici le scroll est local,
|
||
pas de conflit). Leçon : quand une liste clé→série reçoit une nouvelle
|
||
clé, GREPPER chaque consommateur de la liste (légende, stats, styles).
|
||
|
||
**Leçons** : (a) ne jamais croire un build « probablement bon » sans l'avoir lancé ;
|
||
(b) les tests sémantiques attrapent ce que la compilation ne voit pas ; (c) se méfier des
|
||
constantes stdlib « de mémoire » (`ln2`), des mélanges Float/Double, et des APIs M3
|
||
expérimentales sans `@OptIn` ; (d) **un test de régression sur les VRAIES données
|
||
utilisateur** attrape les bugs de convention (casse, unités) que les tests
|
||
synthétiques ratent — mais garde ces données **hors du dépôt** (§8.bis) ;
|
||
(e) attention aux identifiants « presque pareils » entre sources (constantes app
|
||
vs clés d'asset) ; (f) après un échec de COMPILATION, jeter les résultats de tests
|
||
de la même passe (XML périmés) ; (g) tout script de réécriture d'historique doit
|
||
être idempotent ; (h) dans un KDoc, `/**` imbrique ; (i) quand un pattern d'IO
|
||
marche (export JSON), le réutiliser TEL QUEL — une réimplémentation « équivalente »
|
||
perd les garde-fous acquis à l'usage (cf #45) ; (j) vérifier les POSTULATS dans le
|
||
code réel, pas dans la doc (« le manifest contient WRITE_CALENDAR », cf #44) ;
|
||
(k) un bump de version non commité = métadonnées fausses dans les APK publiés
|
||
(cf #46) — le bump fait partie du commit de release ; (l) le scan de
|
||
confidentialité s'ADAPTE automatiquement aux nouveaux exports
|
||
(`local-test-data/`) : un historique « propre hier » peut devenir hit dès
|
||
qu'un nouvel export introduit des motifs qui collent — re-scanner PUIS juger
|
||
avec l'utilisatrice (dette déjà jugée, cf §8.bis) ; (m) **un crash sans stack
|
||
trace = d'abord le reproduire** (émulateur + données réelles, §16.ter) — les
|
||
deux « fixes » aveugles de v1.3.2/v1.3.3 (#41/#45) ont laissé passer un bug
|
||
trivial (#48) seulement visible sur l'émulateur ; (n) `./gradlew lint` fait
|
||
désormais partie de la vérification avant release : `StringFormatMatches`
|
||
aurait signalé #47 dès v1.3.1 ; (o) **les tests unitaires JVM ne voient
|
||
JAMAIS la minification R8 — la recette §16.ter sur l'APK RELEASE est la
|
||
seule filet** (bug #64 : feature parfaite en debug, vide en release) ;
|
||
(p) quand une classe pure disparaît du mapping R8 (`R8$$REMOVED$$CLASS`),
|
||
la garder explicitement dans proguard-rules.pro plutôt que débattre avec
|
||
l'optimiseur.
|
||
|
||
### #64 (v1.6.0) — « Tracé labs » vide EN RELEASE SEULE (R8 full mode)
|
||
|
||
**Symptôme** (émulateur §16.ter, APK release v1.6.0, données réelles seedées) :
|
||
le chip « Lab track » s'active MAIS la courbe n'est ni dessinée (0 pixel
|
||
#C2185B au screencap) ni légendée ; « Prolonger » idem. En DEBUG, le même
|
||
chip sur les mêmes données fonctionne (légende + 7090 px rose après fix).
|
||
**Les 191 tests JVM de l'époque sont verts** (259 aujourd'hui — ils ne passent JAMAIS par R8).
|
||
|
||
**Diagnostic** (méthode de bisection debug/release puis `mapping.txt`) :
|
||
`com.hormonetrack.pk.LabTrajectoryModel -> R8$$REMOVED$$CLASS$$…` — R8 full
|
||
mode a SUPPRIMÉ la classe en inlinant `computeLabAnchoredCurve` (et son
|
||
synthétique `computeLabAnchoredCurve$default` qui gère les paramètres par
|
||
défaut) dans le call site du producer Compose ; après fusion, le calcul
|
||
renvoyait une courbe vide. Les courbes des modèles (même moteur e2At,
|
||
même fichier) fonctionnaient — seul le chemin inliné cassait.
|
||
|
||
**FIX** : `-keep class com.hormonetrack.pk.LabTrajectoryModel { *; }` dans
|
||
proguard-rules.pro (garde commentée + renvoi au mapping). Validé émulateur
|
||
sur l'APK release re-buildé : ancrée (courbe + légende), prolongée (légende
|
||
+ avertissement), 0 crash.
|
||
|
||
**#65/#66 (v1.8.2) — courbes d'accueil/graphiques pas rafraîchies sur édition** : les
|
||
clés des `produceState` étaient incomplètes (Home : `allTreatments`/`tConfig` absentes ;
|
||
Chart : `labResults.size` au lieu de la liste) — éditer un scaleFactor/le modèle T/la
|
||
valeur d'un lab ne rafraîchissait pas les courbes avant le tick de 60 s. Leçon :
|
||
les clés d'un producteur d'état Compose doivent couvrir TOUTE entrée du calcul (pas
|
||
un proxy comme `.size`).
|
||
|
||
**#67 (v1.9.5) — ticks X alignés sur le mauvais fuseau + dérive DST** : `drawXLabels`
|
||
formatait les labels dans le fuseau CHOISI mais appelait `xLabelTicks(t0, t1, step)`
|
||
sans lui passer la zone — l'alignement « minuit local » (fix #55) se faisait sur le
|
||
fuseau de l'APPAREIL (heures décalées dès que les deux divergent). Accessoirement le
|
||
pas avançait en ms fixes (`t += stepMs`) : dérive d'1 h aux transitions DST. Leçon :
|
||
quand un helper prend un fuseau pour FORMATTER, la même zone doit régler
|
||
l'ALIGNEMENT — et un pas « 24 h » s'ajoute via Calendar (minuit LOCAL), pas en ms.
|
||
|
||
**#68 (v1.10.0) — recommandation de prise de sang antérieure à la stabilisation** :
|
||
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 quelques
|
||
heures après l'injection PRÉCÉDENTE (creux d'absorption) : la date proposée était
|
||
gouvernée par l'injection N−1 (antérieure à la stabilisation) et le texte « juste
|
||
avant ton injection du … » était faux de plusieurs jours. Réel : creux 5 oct 02:32
|
||
vs « pas stabilisée avant » 7 oct 04:03. FIX : creux = niveau PRÉ-INJECTION du
|
||
créneau (restaure le principe v1.8.0 ; pour EV le point pré-injection EST le min de
|
||
fenêtre → inchangé). Leçon : quand une valeur affichée est QUALIFIÉE par un texte
|
||
(« juste avant l'injection »), le calcul doit produire CETTE valeur — et un
|
||
minimum de fenêtre n'est pas un creux pré-injection si la courbe remonte dans
|
||
l'intervalle. Épinglé par RegressionUserCase6Test (balayage de `now` ±60 j :
|
||
l'invariant doit tenir à tout instant, pas seulement à l'instant du rapport).
|
||
|
||
**#69 (v1.10.0) — dialog de dose fantôme après une notification** : tap
|
||
notification → dialog de saisie ; le fermer puis changer d'onglet et revenir sur
|
||
l'accueil le ROUVRAIT à chaque fois. Cause : la demande vivait au niveau ACTIVITÉ
|
||
(paramètre d'intent) tandis que 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 rouvrait. FIX : `MutableState<Long?>` détenu par
|
||
MainActivity, consommé UNE fois ; extras retirés de l'intent après lecture (un
|
||
recreate ne rouvre pas une notification traitée) ; bonus : tap notification
|
||
app-ouverte ouvre désormais aussi le dialog. Leçon : une demande à consommer une
|
||
fois doit vivre à la MÊME durée de vie que sa consommation — jamais « paramètre
|
||
longue-vie + effet courte-vie ». Épinglé par le test UI LogDoseRequestTest.
|
||
|
||
**#70 (v1.12.0) — inactif à Posologie projetait encore ses injections** :
|
||
`generateForecastDoses` (source unique des créneaux prévisionnels) ne vérifiait
|
||
pas l'archivage — un injectable INACTIF avec Posologie et de l'historique
|
||
continuait d'alimenter le graphique Prévision, l'extension de fenêtre et
|
||
l'horizon LabTiming ; accessoirement le chip Prévision s'activait sur des
|
||
Posologies inactives. FIX : garde `!isActive` dans le moteur + horizon/chip sur
|
||
les ACTIFS. Cohérence d'ensemble (§6.bis) : l'INACTIF garde son historique
|
||
simulé/calibré mais ne projette plus rien — exactement comme ses rappels
|
||
(v1.4.0). Leçon : quand un état administratif gouverne DÉJÀ une consommation
|
||
d'une source (rappels), vérifier les AUTRES consommateurs de la même source.
|
||
Épinglé par ForecastDosesGuardTest (3 tests : inactif vide, actif inchangé,
|
||
avant/après archivage).
|
||
|
||
## 15.bis Comment régénérer l'asset mcmc_samples.json (v1.9.0)
|
||
|
||
L'asset `src/main/assets/mcmc_samples.json` (posterior MCMC de
|
||
estrannaise.js : 313 échantillons `(d, k1, k2, k3)` par ester injectable)
|
||
est extrait de `src/modeldata.js` du dépôt
|
||
https://github.com/WHSAH/estrannaise.js/ :
|
||
1. cloner le dépôt (branche main) ;
|
||
2. convertir les exports JS en JSON (node : importer `mcmcSamplesPK`, dump JSON) ;
|
||
3. mapper les clés du repo vers les clés de l'app : « EV im »→EV, « EUn im »→EU,
|
||
« EEn im »→EEn, « EC im »→EC, « EB im »→EB, « EUn casubq »→EUCS ;
|
||
4. écrire `{ester: [[d,k1,k2,k3] × 313]}` (compact, ≈ 48 Ko) ;
|
||
5. le miroir web lit `assets/mcmc_samples.json` du dépôt web (même fichier).
|
||
|
||
## 15. Comment régénérer l'asset pk_profiles.json
|
||
|
||
Si le `.ods` change (re-fits, nouveaux esters) :
|
||
|
||
```python
|
||
# python3 stdlib only :
|
||
# 1. zipfile.ZipFile(ods).read("content.xml")
|
||
# 2. ElementTree (ns table/office/text) → table "Models"
|
||
# 3. lignes 1-4 = D, k1, k2, k3 (colonnes EV/EU/EEn ese + tfs) — informatif, non utilisé
|
||
# 4. lignes 5+ = profils horaires (00:00 … 8000:00), décimaux FR "61,12" → float
|
||
# 5. json.dump({"params": …, "profiles": {"EV_ese": [8001], "EU_ese": …, "EEn_ese": …,
|
||
# "EV_tfs": …, "EU_tfs": …, "EEn_tfs": …}})
|
||
# 6. cp vers app/src/main/assets/pk_profiles.json
|
||
# 7. vérifier : 6 clés × 8001 valeurs, pics == référence (§7.1) ; les tests le vérifient
|
||
```
|
||
|
||
Le script de la session 1 a été exécuté inline (non archivé) — le refaire depuis la
|
||
structure ci-dessus. **Toute restructuration du JSON impose de mettre à jour
|
||
`PKProfileStore.initWithJson`.**
|
||
|
||
## 16. Workflow build / test / install / git
|
||
|
||
```bash
|
||
cd ~/projects/HormoneTrack
|
||
./gradlew assembleDebug testDebugUnitTest # build + 259 tests (225 sans données locales)
|
||
./gradlew assembleRelease # APK optimisé R8 (cf §16.bis ; signature cf §16.quater)
|
||
./gradlew connectedDebugAndroidTest # 14 tests UI Compose (émulateur requis, §8.ter)
|
||
adb install -r app/build/outputs/apk/debug/app-debug.apk
|
||
```
|
||
|
||
**Git (initialisé le 2026-09-05, branche `main`, DEUX remotes Gitea)** :
|
||
- `origin` → `https://gitea.cloudyfy.fr/Siphonight/HormoneTrack` (privé, HTTPS + trousseau)
|
||
- `farewell` → `git@farewell:Siphonight/HormoneTrack.git` (SSH, alias `farewell`
|
||
= `giteassh.farewell.dev:2222` avec clé dédiée, cf `~/.ssh/config`) —
|
||
**repo créé + push + LES 12 RELEASES publiées avec APK vérifiés par
|
||
téléchargement (2026-09-06)** ; token `gitea.farewell.dev` au trousseau
|
||
(scope write:repository)
|
||
- Historique = commits logiques par couche (toolchain / moteur / UI / docs) ;
|
||
- **Chaque release = tag annoté** (`v1.1.0`, `v1.2.0`, …) :
|
||
`git tag -a vX.Y.Z -m "…" && git tag` pour lister ;
|
||
- `local.properties`, `build/`, `.gradle/`, `.idea/`, **`local-test-data/`** sont
|
||
ignorés (`.gitignore`) — ce dernier protège les données de santé de test ;
|
||
- Avant chaque commit de release : `./gradlew testDebugUnitTest` doit être vert ;
|
||
- Push : `git push origin main --tags` + `git push farewell main --tags`
|
||
(farewell : auth SSH par clé dédiée ; le push-to-create y est
|
||
DÉSACTIVÉ → le repo doit exister au préalable sur l'instance).
|
||
⚠️ Tokens Gitea : scope `write:repository` suffit pour push, releases et
|
||
assets ; il NE permet PAS de créer un repo via API (il faut `write:user`)
|
||
ni d'utiliser push-to-create.
|
||
|
||
### 16.bis Releases Gitea avec APK téléchargeable
|
||
|
||
Les releases Gitea embarquent l'APK de chaque version — les utilisateurs n'ont pas
|
||
besoin de compiler. **Le corps de chaque release = la section CHANGELOG de la
|
||
version** (markdown rendu par Gitea), extrait automatiquement.
|
||
|
||
Le tout est automatisé par `scripts/gitea-release.py` — **multi-instances**
|
||
(`cloudyfy` par défaut, `farewell`) :
|
||
|
||
```bash
|
||
# 0. Le CHANGELOG est embarqué dans l'app (dialog « Nouveautés ») :
|
||
# la tâche Gradle copyChangelog copie docs/CHANGELOG.md vers
|
||
# src/main/assets/changelog.md à chaque build (gitignoré, auto)
|
||
|
||
# 1. Construire les DEUX APK au niveau du tag (debug + release R8)
|
||
git checkout vX.Y.Z
|
||
./gradlew assembleRelease assembleDebug # release = recommandé (R8, cf plus bas)
|
||
cp app/build/outputs/apk/release/app-release.apk /tmp/apks/HormoneTrack-vX.Y.Z-release.apk
|
||
cp app/build/outputs/apk/debug/app-debug.apk /tmp/apks/HormoneTrack-vX.Y.Z-debug.apk
|
||
git checkout main
|
||
|
||
# 2. Publier sur chaque instance (corps = CHANGELOG + les 2 APK attachés)
|
||
# ⚠️ NE JAMAIS comparer les tags en chaînes (v1.2.10 < v1.2.5
|
||
# lexicographiquement !) — comparer en tuples numériques (§14 #38)
|
||
# ⚠️ NE JAMAIS utiliser gitea-release.py en deux invocations rapprochées
|
||
# (les uploads se remplacent mutuellement, confirmé 4 fois) :
|
||
# → UNE invocation de **scripts/publish-release.py** par instance fait
|
||
# tout (purge + upload des 2 APK + vérification par téléchargement).
|
||
python3 scripts/publish-release.py cloudyfy vX.Y.Z
|
||
python3 scripts/publish-release.py farewell vX.Y.Z
|
||
```
|
||
|
||
Le script choisit l'instance (URL, owner) et lit le token Gitea correspondant
|
||
dans le trousseau macOS (`security find-internet-password -s <hôte> -w`) :
|
||
`gitea.cloudyfy.fr` ✓ présent ; `gitea.farewell.dev` → à ajouter :
|
||
`security add-internet-password -s gitea.farewell.dev -a Siphonight -w <TOKEN> -U`.
|
||
|
||
Le script :
|
||
- extrait la section `## [X.Y.Z]` de `docs/CHANGELOG.md` comme corps ;
|
||
⚠️ piège : le tag git porte le « v » (`v1.2.3`) mais la CHANGELOG non
|
||
(`[1.2.3]`) — première version du script cherchait `[vX.Y.Z]` et tombait sur
|
||
le fallback « Voir docs/CHANGELOG.md » ;
|
||
- crée la release si absente, sinon **met à jour le corps** (PATCH) ;
|
||
- attache l'APK (remplace l'asset du même nom si présent) — ⚠️ script
|
||
mono-APK : ne plus l'utiliser pour publier les APK, cf #43 et
|
||
publish-release.py (UNE invocation fait tout) ;
|
||
⚠️ vérification intégrée (`ensure_asset`) : le `?name=` de l'upload et/ou
|
||
le PATCH peuvent être ignorés par les instances (assets au nom générique /
|
||
APK disparu — v1.2.6, v1.2.10) → le script vérifie **nom ET taille** après
|
||
chaque upload, retente le PATCH une fois, et **échoue bruyamment** si
|
||
l'asset ne colle pas — ne pas retirer ;
|
||
- **Releases antérieures à v1.2.5 = APK debug unique** : backfill des APK
|
||
release abandonné (best effort) — les anciens `build.gradle.kts` sortaient
|
||
un `app-release-unsigned.apk` non signé (pas de signing config à l'époque).
|
||
Les utilisateurs prennent la dernière version.
|
||
`HormoneTrack-vX.Y.Z-release.apk` (**recommandé**, R8) et
|
||
`HormoneTrack-vX.Y.Z-debug.apk` ;
|
||
- lit le token Gitea dans le trousseau macOS (`security find-internet-password`) ;
|
||
scope requis : `write:repository` (suffit pour releases + assets, pas pour
|
||
créer un repo — cf plus haut).
|
||
|
||
**Build release (v1.2.5, signature conditionnelle depuis v1.9.8)** :
|
||
`./gradlew assembleRelease` — R8 full mode + `isShrinkResources`
|
||
(20 Mo → 2,7 Mo). **Signature** : si `keystore.properties` existe à la racine
|
||
du dépôt (gitignoré, créé par `scripts/make-release-keystore.sh` — cf §16.quater),
|
||
la build release utilise CE keystore ; sinon elle reste signée avec la **clé
|
||
debug** (même signature que les APK debug distribués → mise à jour par-dessus
|
||
sans perte de données — comportement historique). Garde-fous dans
|
||
`proguard-rules.pro` : Gson lit les champs des modèles par réflexion →
|
||
`-keep` explicites sur `data.model.**`, `BackupData`, `TConfig` (+ attributs
|
||
`Signature`) — sinon l'export/import JSON casse UNIQUEMENT en release.
|
||
⚠️ À chaque activation d'optimisation : tester sur téléphone l'export/import
|
||
de backup + les graphiques (R8 ne se vérifie pas en tests JVM).
|
||
|
||
### 16.quater Signature release : keystore dédié (v1.9.8)
|
||
|
||
Historique : les APK release étaient signés avec la **clé debug** (choix
|
||
v1.2.5 — installer par-dessus l'APK debug sans perte de données). Le jour où
|
||
l'app est distribuée plus largement (dépôt public, F-Droid…), une vraie clé
|
||
de signature est requise — elle est désormais câblée :
|
||
|
||
- **Création (une fois)** : `scripts/make-release-keystore.sh` — génère
|
||
`release.keystore` (RSA 4096, alias `hormonetrack`, validité 10 000 j) +
|
||
`keystore.properties` à la racine, TOUS DEUX GITIGNORÉS (`.gitignore` :
|
||
`keystore.properties`, `*.keystore`, `*.jks`). Le script refuse
|
||
d'écraser une paire existante (une régénération = perte de la capacité
|
||
de mise à jour des installs existantes). Mots de passe saisis
|
||
interactivement (jamais en argument, jamais committés) ;
|
||
- **Effet** : dès que `keystore.properties` existe, `assembleRelease` signe
|
||
avec ce keystore (détection dans `app/build.gradle.kts`) ; sans le
|
||
fichier, fallback clé debug inchangé ;
|
||
- **⚠️ MIGRATION téléphone obligatoire** (signature ≠ clé debug ⇒ Android
|
||
refuse la mise à jour par-dessus) :
|
||
1. exporter le JSON (Paramètres → Sauvegarde → Exporter, ou auto-backup) ;
|
||
2. désinstaller l'app ;
|
||
3. installer l'APK release signé par le nouveau keystore ;
|
||
4. réimporter le JSON. La base Room ne voyage QUE par le JSON ;
|
||
- **Sauvegarde du keystore** : le copier hors du dépôt (coffre, second
|
||
disque) — sa perte = même migration subie par TOUS les utilisateurs à la
|
||
version suivante ;
|
||
- **Checklist avant release publiée avec la nouvelle clé** : build release →
|
||
`apksigner verify --print-certs` (l'empreinte doit être celle du NOUVEAU
|
||
keystore) → smoke test émulateur §16.ter → publication + note de migration
|
||
dans le corps de la release Gitea.
|
||
|
||
### Checklist de déploiement complète (de A à Z, pour une session sans contexte)
|
||
|
||
1. **Bumper la version** dans `app/build.gradle.kts` : `versionCode = N+1`,
|
||
`versionName = "X.Y.Z+1"` (SemVer : fix = Z, feature = Y).
|
||
2. **Tests verts obligatoires** : `./gradlew testDebugUnitTest` — 274 au
|
||
total, 240 si `local-test-data/` est absent (les 6 classes de régression
|
||
réelles sont skippées via `Assume`) ; **lint vert
|
||
obligatoire** : `./gradlew lint` (v1.3.4 — `StringFormatMatches` aurait
|
||
attrapé les crashs #47/#48 dès v1.3.1).
|
||
**v1.9.8+ — tests UI obligatoires** : `./gradlew connectedDebugAndroidTest`
|
||
(14 tests §8.ter — émulateur requis).
|
||
**v1.10.0+ — miroir web** : si la session touche une logique miroirée
|
||
(lab-timing, moteur…), `bash scripts/check.sh` dans `~/projects/HormoneTrack-web`
|
||
doit être vert (180 tests + E2E) AVANT le tag. Paliers de test du projet :
|
||
chaque commit = tests + lint ; release = + tests UI + scan confidentialité
|
||
+ APK au tag ; crash reporté = reproduction émulateur (§16.ter) AVANT tout fix.
|
||
3. **Docs** : section `## [X.Y.Z]` en tête de `docs/CHANGELOG.md` (le corps des
|
||
releases Gitea en sera extrait automatiquement par le script), + §14 si bug
|
||
corrigé, + §2 (historique) si notable.
|
||
3.bis. **⚠️ VALIDATION ÉMULATEUR §16.ter — OBLIGATOIRE AVANT LE TAG (leçon
|
||
#64, v1.6.0 : oubliée puis rattrapée, un bug release-only est passé
|
||
jusque dans l'APK taggué)** : recette complète §16.ter (émulateur
|
||
headless hrt36 + seed `scripts/seed-emulator.py` avec le dernier
|
||
backup de `local-test-data/` + APK release installé PAR-DESSUS) sur
|
||
**l'APK RELEASE** (JAMAIS le debug seul : **R8 ne se vérifie qu'ici** —
|
||
les 259 tests JVM ne le voient jamais, cf #64). Parcours minimum :
|
||
l'app démarre 0 crash → écran Graphiques → activer les chips de la
|
||
release (rangée modèles défilable : `adb shell input swipe 900 716 150
|
||
716 400` puis tap aux bounds du `uiautomator dump`) → vérifier dans le
|
||
dump `text=` que la légende attendue apparaît → `adb logcat -b crash
|
||
-d | grep -c FATAL` reste à 0 (les **259** tests JVM ne voient jamais
|
||
R8, cf #64). Limites : le pan/pinch n'est PAS
|
||
testable par adb (§16.ter 4.bis) — le geste reste validé par
|
||
l'utilisatrice sur Pixel 9.
|
||
4. **Commit** (message descriptif par couche) + **tag annoté** :
|
||
`git tag -a vX.Y.Z -m "…"`. ⚠️ Si un fix arrive APRÈS le tag mais
|
||
AVANT la publication d'APK (cas #64) : AUCUNE release n'existe encore →
|
||
`git tag -d vX.Y.Z` + re-tag sur le commit de fix + `push --tags`
|
||
(jamais de re-tag sur une release PUBLIÉE : bump en X.Y.(Z+1) à la
|
||
place).
|
||
4.bis. **Release SYNC web** (depuis le 8 sept. 2026) : le portage web vit
|
||
dans son PROPRE dépôt `~/projects/HormoneTrack-web` (2 Gitea,
|
||
`Siphonight/HormoneTrack-web` — historique 100 % propre, séparé de la
|
||
dette de confidentialité de ce dépôt). À chaque release : porter la
|
||
feature web si pas déjà fait, aligner `WEB_VERSION` sur la MÊME `X.Y.Z`,
|
||
`bash scripts/check.sh` vert (155 tests + E2E), commit + tag `vX.Y.Z`
|
||
côté web, push des deux dépôts. Changelogs TOTALEMENT SÉPARÉS
|
||
(`web/docs/CHANGELOG.md` ≠ ce fichier). Process web complet :
|
||
`../HormoneTrack-web/docs/DEVELOPPEMENT.md` §3 + §10. Publication
|
||
Gitea web GELÉE tant que la parité qualité n'est pas validée
|
||
(commit + tag seulement).
|
||
5. **Vérif confidentialité** : `git ls-tree -r <tag> --name-only | grep local-test-data` → VIDE + scan de confidentialité (déjà exigé avant tout push, §8.bis) ; puis **Push** : `git push origin main --tags` + `git push farewell main --tags`
|
||
(farewell : le repo doit exister sur l'instance ; push-to-create désactivé).
|
||
6. **Construire les 2 APK au niveau du tag** :
|
||
`git checkout vX.Y.Z` → `./gradlew assembleRelease assembleDebug` → copier
|
||
`app-release.apk` → `/tmp/apks/HormoneTrack-vX.Y.Z-release.apk` et
|
||
`app-debug.apk` → `HormoneTrack-vX.Y.Z-debug.apk` → `git checkout main`.
|
||
7. **Publier les releases** (corps = section CHANGELOG + les 2 APK attachés) :
|
||
`python3 scripts/publish-release.py cloudyfy vX.Y.Z` puis idem avec
|
||
`farewell` (⚠️ UNE invocation = les 2 APK + vérification par
|
||
téléchargement — NE PAS utiliser gitea-release.py deux fois de suite,
|
||
les uploads rapprochés se remplacent mutuellement, cf §14 #43 ;
|
||
token `gitea.farewell.dev` requis dans le trousseau).
|
||
8. **Smoke-test R8 sur téléphone** (le release APK n'est pas vérifiable en
|
||
tests JVM) : installation par-dessus l'existant, graphiques, export ET
|
||
import d'un backup JSON, rappel. Le debug APK est le repli (même signature).
|
||
9. **Mettre à jour §2/§8/§19/§21 si besoin** puis pusher la doc.
|
||
|
||
Notes :
|
||
- L'APK est une **build debug** signée avec la clé debug locale — installable en
|
||
sideload, mises à jour entre versions OK (même signature) ;
|
||
- Le dépôt est **privé** : le téléchargement des releases exige d'être connecté ;
|
||
rendre le dépôt public rend les APK téléchargeables sans compte (aucune donnée
|
||
de santé dans le dépôt, cf §8.bis).
|
||
|
||
- Téléphone : mode développeur + Débogage USB (détails : GUIDE_INSTALLATION.md)
|
||
- À ma charge (assistant) : build + tests JVM ✓ ; **émulateur installé et
|
||
opérationnel depuis v1.3.4 (recette §16.ter)** ;
|
||
**les tests humains sur vrai téléphone restent la référence**
|
||
(notifs → montre, UX de saisie, pickers, panoramique du chart)
|
||
|
||
### 16.ter Recette : test manuel sur émulateur (reproductible — v1.3.4)
|
||
|
||
But : reproduire/valider un crash ou une UI sur l'app RÉELLE (sans téléphone
|
||
branché) — c'est cette recette qui a diagnostiqué les crashs #47/#48 en une
|
||
session. ⚠️ AUCUNE donnée personnelle n'est embarquée : le backup utilisé vit
|
||
sous `local-test-data/` (gitignoré, §8.bis) et est passé au script en argument.
|
||
|
||
**0. Installation (one-shot, cf §4)** : `sdkmanager "emulator"
|
||
"system-images;android-31;aosp_atd;arm64-v8a"` (léger, pour le LOGIQUE sans
|
||
SAF) et/ou `"system-images;android-36;google_apis;arm64-v8a"` (image COMPLÈTE,
|
||
nécessaire pour piloter les sélecteurs de fichiers). AVD :
|
||
`avdmanager create avd -n hrt36 -k "system-images;…" -d pixel_6` (l'erreur
|
||
béninie « devices.xml » ne bloque pas la création). Boot headless :
|
||
`emulator -avd hrt36 -no-window -no-audio -no-boot-anim -no-snapshot
|
||
-gpu swiftshader_indirect &` puis `adb wait-for-device` + attente
|
||
`getprop sys.boot_completed`.
|
||
|
||
**1. Seed des « données réelles »** — la DB est modifiable uniquement sur un
|
||
APK DEBUGGABLE (`run-as`) :
|
||
|
||
```bash
|
||
adb install app/build/outputs/apk/debug/app-debug.apk
|
||
adb shell am start -n com.hormonetrack/.MainActivity # 1er lancement → crée la DB
|
||
adb shell am force-stop com.hormonetrack # flush de la DB
|
||
adb shell run-as com.hormonetrack cat databases/hormonetrack.db > /tmp/hrt.db
|
||
python3 scripts/seed-emulator.py /tmp/hrt.db local-test-data/backup-v1.3.1.json
|
||
adb push /tmp/hrt.db /data/local/tmp/hrt.db
|
||
adb shell run-as com.hormonetrack cp /data/local/tmp/hrt.db databases/hormonetrack.db
|
||
adb shell run-as com.hormonetrack rm -f databases/hormonetrack.db-wal \
|
||
databases/hormonetrack.db-shm # WAL périmé sinon ! (checkpoint implicite fait par le seed)
|
||
```
|
||
|
||
⚠️ **Piège WAL constaté à la v1.9.8** : après un `force-stop` (= SIGKILL, PAS de
|
||
checkpoint), le schéma entier peut vivre SEULEMENT dans le `-wal` — extraire la
|
||
seule `hormonetrack.db` donne un fichier de 4 Ko SANS tables (`no such table:
|
||
treatments` au seed). Solution : extraire `hormonetrack.db` **ET**
|
||
`hormonetrack.db-wal` CÔTE À CÔTE sous leurs noms d'origine (ex. `/tmp/hrtseed/`)
|
||
— sqlite rejoue le WAL à l'ouverture et le fusionne à la fermeture (le `-wal`
|
||
disparaît après le seed) ; on pousse alors la seule `.db` résultat.
|
||
|
||
Règles du seed (le script les impose) : colonnes = noms de propriétés Kotlin
|
||
(Room n'applique pas de snake_case), enums stockés en String — on insère
|
||
SEULEMENT des lignes, on ne touche NI au schéma NI à `room_master_table`
|
||
(Room valide le schéma à l'ouverture, pas les lignes).
|
||
|
||
**2. Passer en RELEASE (c'est LE build à tester)** — `install -r` PAR-DESSUS
|
||
le debug CONSERVE la DB (les APK release/signés debug sont la même clé) :
|
||
|
||
```bash
|
||
adb install -r app/build/outputs/apk/release/app-release.apk
|
||
adb logcat -c && adb shell am start -n com.hormonetrack/.MainActivity
|
||
```
|
||
|
||
**3. Piloter l'UI headless** : à l'aide de `uiautomator dump` + `input tap` —
|
||
l'UI se pilote SANS écran :
|
||
|
||
```bash
|
||
adb shell uiautomator dump /sdcard/ui.xml && adb shell cat /sdcard/ui.xml
|
||
# → parser text=…/content-desc=… + bounds=[x1,y1][x2,y2] → centre (cx,cy)
|
||
adb shell input tap <cx> <cy> # naviguer (5 tabs ≈ y=2280 à 1080×2400)
|
||
adb shell input keyevent KEYCODE_BACK
|
||
```
|
||
|
||
**4. Attraper le crash** : le process meurt et repasse au launcher — c'est le
|
||
signal ; stack : `adb logcat -b crash -d` (grep FATAL). **0 crash attendu :
|
||
`grep -c FATAL` reste à 0** après chaque interaction.
|
||
|
||
**4.bis LIMITATION des gestes dans l'émulateur headless (v1.4.10)** : les
|
||
`adb shell input swipe` ne délivrent PAS les mouvements intermédiaires au
|
||
chart (`detectTransformGestures` ne reçoit que down/up sans moves) → le
|
||
pan et le pinch sont INTESTABLES par adb sur cet environnement (bug #62
|
||
non détectable ici). Le pan/pinch se valide sur le Pixel 9 réel (l'appareil du test humain). Les TAPS, la navigation et les dialogs fonctionnent
|
||
normalement par adb.
|
||
|
||
**5. Flux SAF (export JSON/logs)** : sur android-31 ATD le
|
||
DocumentsUI est STUBBÉ (`fakesystemapp`) → INUTILISABLE ; utiliser l'image
|
||
android-36 google_apis. Le picker se pilote pareil (bouton `SAVE` en bas à
|
||
droite) ; vérification : fichier présent dans `/sdcard/Download/` + le message
|
||
de confirmation (v1.3.3 : « Logs de diagnostic exportés ») visible via un
|
||
nouveau dump UI.
|
||
|
||
**6. Vérifications post-fix** : Doses (en-tête « temps sous THS » + liste),
|
||
Settings → Export logs (SAF + SAVE + message), export/import JSON, changelog
|
||
dialog au premier lancement (sinon re-seed un `changelog_seen_version`).
|
||
⚠️ Les coordonnées UI (x/y) évoluent avec l'écran/l'app : TOUJOURS re-dumper
|
||
`uiautomator` avant de taper — ne jamais figer des coordonnées hors dump.
|
||
|
||
## 17. Montre : Gadgetbridge & options
|
||
|
||
Doc dédiée : [MONTRE-GADGETBRIDGE.md](MONTRE-GADGETBRIDGE.md). Synthèse :
|
||
|
||
- GT 3 = Lite Wearable ; GB supporte la GT 3 (« mostly supported ») : **notifications ✓,
|
||
watchfaces `.hwt` ✓, apps `.hap` ✗**
|
||
- Health et GB ne peuvent pas être appairés simultanément
|
||
- Watchface via GB : **aucune signature requise** ; app `.hap` : certificat debug AGC +
|
||
UDID (chaîne DevEco Studio → DevEco Assistant)
|
||
- Régression connue : HarmonyOS 6.1+ casse l'install `.hwt` via GB (issues #5968/#6005/#6199) ;
|
||
GT 3 en HarmonyOS 4.0.0.120 probablement OK, à valider
|
||
- Choix v1 : notifications via GB/Health ; Phase 2 : watchface custom (statique) ou
|
||
mini-app Lite Wearable autonome (Wear Engine = accès partenaire)
|
||
|
||
## 18. Espace disque & coûts
|
||
|
||
Mesuré le 5 sept. 2026 (Mac, 228 Go, **33 Go libres** au départ) :
|
||
|
||
| Élément | Taille |
|
||
|---|---|
|
||
| SDK Android (cmdline-tools + platforms 34/36/**37** + build-tools 34/36/37 + platform-tools) | ≈ 700 MB |
|
||
| Cache Gradle (~/.gradle, plusieurs distributions 8.9→9.7.1 + deps AGP 9/Compose 1.12) | ≈ 3–4 GB |
|
||
| Projet (sources + build outputs) | ≈ 100 MB |
|
||
| **Total outillage actuel** | **≈ 4–5 GB** |
|
||
|
||
Marges : émulateur + image système ≈ +2–3 GB ; DevEco Studio (Phase 2) ≈ +10 GB →
|
||
tout rentre très largement. Note : **AGP télécharge automatiquement les plateformes
|
||
manquantes** (licences signées) — c'est comme ça que android-37 est arrivé.
|
||
|
||
## 19. Limites connues
|
||
|
||
Volontaires (v1) :
|
||
- Pas de ViewModel/DI (couplage UI↔repo via CompositionLocal)
|
||
- Modèle T empirique (non publié) — étiqueté estimation partout
|
||
- Import JSON = **écrasement** depuis v1.2.6 (mode fusion non implémenté)
|
||
- Profils par **tables** (pas par formule) : les D/k1–k3 de l'ODS ne sont pas consommés —
|
||
rétro-ingénierie des fits non tentée ; les tables sont exactes
|
||
- DST : les rappels quotidiens peuvent glisser d'1 h après changement d'heure, jusqu'au
|
||
prochain reschedule (boot/save) — mineur
|
||
- Labs : marqueur libre — E2/T exacts requis pour calibration/charts
|
||
- `allowBackup=false` → backups = export JSON manuel + auto-backup quotidien (v1.7.0, dans le dossier choisi)
|
||
- **APK release (R8)** : la réflexion Gson est couverte par des `-keep`
|
||
explicites, mais R8 ne se vérifie pas en tests JVM → **smoke-test sur
|
||
téléphone** (export/import backup, graphiques) avant chaque publication ;
|
||
signature : clé debug PAR DÉFAUT, keystore dédié dès que `keystore.properties`
|
||
existe (v1.9.8, §16.quater — migration backup→désinstall→réimport requise)
|
||
- **ChartScreen reste un fichier unique (790 lignes)** : la garde de source
|
||
#54 (ChartScreenSourceGuardTest) exige le bloc de gestes + les alias
|
||
`gesture*` dans CE fichier — découpage = réécriture de la garde + risque
|
||
de réintroduction des captures figées. SettingsScreen a été découpé
|
||
(v1.9.8, §10) ; le chart attend une refonte dédiée
|
||
- **Grille de rappel sous-quotidienne** (v1.4.0) : une Posologie < 1 j
|
||
(ex. 2 injections/jour) + UNE heure de rappel → l'app ne peut exprimer
|
||
qu'un rappel/jour à heure fixe les jours de créneau (épinglé par
|
||
`ReminderScheduleTest`) ; deux heures de rappel distinctes = évolution
|
||
- **Lint (v1.3.4)** : `./gradlew lint` est vert — `StringFormatMatches` et
|
||
famille restent des ERREURS bloquantes ; les checks Compose 1.12+
|
||
théoriques (`NonObservableLocale`, `LocalContextGetResourceValueCall`,
|
||
staleness de configuration) sont rétrogradés en WARNING via `app/lint.xml`
|
||
→ à re-traiter lors d'une refonte i18n (§20)
|
||
|
||
## 20. Idées d'évolution
|
||
|
||
1. **Robolectric** (VM Android en JVM — pas besoin d'appareil ; les tests
|
||
Compose instrumentés existent depuis v1.9.8, §8.ter)
|
||
2. **Étendre les tests UI** : dialogs dose/lab avec données seedées (§16.ter),
|
||
parcours complet (créer un traitement → loguer une dose → courbe visible) —
|
||
les tests actuels sont volontairement data-indépendants
|
||
3. Mode « planifier les injections » (schedule récurrent → pré-remplir le log)
|
||
4. Import JSON : détection de doublons / mode fusion optionnel (l'écrasement est fait, v1.2.6)
|
||
5. Verrou biométrique (BiometricPrompt), widget, export CSV
|
||
6. Charts : tooltip au toucher (le pan est fait v1.2.0, **le zoom v1.2.9**) ;
|
||
MaterialExpressiveTheme quand l'API passera publique (cf §3)
|
||
7. ~~Vrai keystore de distribution~~ **FAIT v1.9.8** (infra complète, §16.quater :
|
||
script + détection Gradle + migration documentée — la bascule réelle se
|
||
fera quand la distribution s'élargira)
|
||
|
||
Fait (à ne pas refaire) : pan du chart (v1.2.0), **zoom du chart (v1.2.9)**,
|
||
pics/creux (v1.2.3), prévision par fréquence (v1.2.0), calibration par période
|
||
d'ester E2 **et** T (v1.2.1/v1.2.3), édition doses (v1.1.0) et labs (v1.2.2),
|
||
E2+T en une entrée (v1.2.2), migration Room v1→v2 sans fallback destructif
|
||
(v1.2.0), rooms v2→v3 (calendarEventId, v1.3.0), dépôt Gitea + releases APK
|
||
(push session), **tests UI Compose + signature release dédiée + découpage
|
||
Settings (v1.9.8)**, **README anglais (v1.9.8)**.
|
||
8. Phase 2 montre : watchface `.hwt` custom, puis mini-app Lite Wearable (cf §17)
|
||
|
||
### 20.bis Pistes d'optimisation écartées ou différées (audit v1.8.2)
|
||
|
||
L'audit complet de v1.8.2 a relevé 4 pistes **délibérément NON appliquées**
|
||
(gain réel mais risque/effort hors critère « comportement identique, sûr ») —
|
||
pour ne pas les redécouvrir et re-reposer les mêmes questions :
|
||
|
||
1. **`importJson` par lots** : les inserts ligne à ligne = une transaction
|
||
Room par ligne. `@Insert` en liste préserverait l'ordre (IDs) et
|
||
accélérerait l'import d'un gros historique. Différé : surface DAO +
|
||
repository à modifier, validation manuelle (aucun test Room auto).
|
||
2. ~~**`BootReceiver` : consolidation `goAsync` + une seule lecture`~~**
|
||
**FAIT v1.10.1** (pattern `goAsync`+coroutine de ReminderReceiver,
|
||
`runBlocking` supprimés du fichier).
|
||
3. **Double évaluation de `generateForecastDoses`** dans ChartScreen
|
||
(producer `forecastDoses` vs producer `curves`) : NE PAS fusionner tel
|
||
quel — le premier utilise `System.currentTimeMillis()`, le second le
|
||
`nowMs` figé au tick ; unifier change l'ensemble des créneaux
|
||
« strictement futurs » à la frontière de la minute (observable).
|
||
À unifier UNIQUEMENT avec une décision explicite sur le nowMs de référence.
|
||
4. **Cache `terminalDecayParameters`** (PKProfileStore) : le scan O(8 001)
|
||
recalculé à chaque `sample()` extrapolé pourrait être mémoïsé (tables
|
||
immuables après init) — coût réel modeste (l'extrapolation ne sert que
|
||
pour dt ≥ 8 001 h) ; introduit de l'état mutuel → à ne faire que si un
|
||
profilage le justifie.
|
||
|
||
## 21. Checklist de test manuel
|
||
|
||
Sur le téléphone de test (à compléter par l'utilisatrice) :
|
||
|
||
- [ ] App se lance sans crash (asset chargé — sinon cf §14.4)
|
||
- [ ] Créer traitement « EV — Estrannaise » 4 mg + rappel 2 min à l'avance
|
||
- [ ] Notif arrive sur le téléphone **et** la GT 3 (via GB ou Health)
|
||
- [ ] « Pris » → dose loguée dans Doses ; « Reporter 1 h » → nouvelle notif 1 h après
|
||
- [ ] Logger 2–3 injections passées → Home affiche E2/T + delta 6 h cohérents
|
||
(4 mg EV → pic ≈ 4×61×scale ≈ 244 pg/mL à scale=1)
|
||
- [ ] Ajouter un lab E2 → « Calibrer avec les analyses » → scaleFactor plausible (0,5–1,2)
|
||
- [ ] Labs T + « Calibrer k » → k mis à jour, courbe T proche des points
|
||
- [ ] Charts 24 h/7 j/30 j, toggles T/labs, axes lisibles
|
||
- [ ] Export JSON → fichier inspectable ; ré-import → compteur correct
|
||
- [ ] Langue FR↔EN↔Système : UI + notifs basculent
|
||
- [ ] Redémarrer le téléphone → rappel reprogrammé (BootReceiver)
|
||
- [ ] Désactiver un rappel → plus de notif (cancel — cf §14.3)
|
||
- [ ] Tester l'installation d'une watchface `.hwt` via Gadgetbridge (pour la Phase 2)
|
||
- [ ] **v1.10.0** : carte « Prochaine prise de sang » — la date du creux est
|
||
POSTÉRIEURE à la mention « pas stabilisée avant le … » (fix #68), et
|
||
cohérente avec le créneau affiché (~juste avant)
|
||
- [ ] **v1.10.0** : logger la dose du jour AVANT l'heure de rappel → la
|
||
notification ne sonne pas ; le rappel suivant repart au créneau suivant
|
||
- [ ] **v1.11.0** : page Traitements — les inactifs sont TOUT EN BAS sous
|
||
l'en-tête « Traitements inactifs », cartes atténuées ; éditer un
|
||
inactif fonctionne toujours
|
||
- [ ] **v1.12.0** : archiver un injectable à Posologie → plus AUCUN créneau
|
||
projeté (chip Prévision : ses injections disparaissent de la projection)
|
||
- [ ] **v1.12.0** : page Doses — chaque ligne porte l'icône de sa forme
|
||
(pilule/seringue/goutte/sparadrap) ; une dose du jour d'une prise de
|
||
sang affiche l'encart 🧪↑ et/ou 🧪↓
|
||
- [ ] **v1.2.x** : graphique panoramique (glisser → passé, bouton « Revenir à maintenant »)
|
||
- [ ] **v1.2.x** : toggles Estrannaise/TFS indépendants (les deux courbes superposées)
|
||
- [ ] **v1.2.x** : chip « Prévision » (configurer la Posologie d'un traitement d'abord)
|
||
- [ ] **v1.2.x** : chip « Pics / creux » (triangles ▲▼ aux extrema E2 et T)
|
||
- [ ] **v1.2.x** : Calibration automatique ON → courbes ajustées depuis les labs
|
||
(par période d'ester si changement d'ester), OFF → valeurs stockées
|
||
- [ ] **v1.2.x** : tap sur le mini-chart de l'accueil → écran Graphiques
|
||
- [ ] **v1.2.x** : édition d'une dose (tap ligne Doses) et d'un lab (tap ligne
|
||
Analyses → sélecteur E2/T si paire) ; suppression par prise entière
|
||
- [ ] **v1.2.x** : prise de sang E2 + T en une entrée (champs optionnels)
|
||
- [ ] **v1.2.4** : passer un traitement à inactif → retiré de « Log rapide » et
|
||
du dropdown des nouvelles doses, rappel annulé, **mais sa simulation reste
|
||
sur le graphique** et la calibration couvre toujours ses périodes
|
||
- [ ] **v1.2.6** : import d'un backup avec l'app déjà remplie → ÉCRASER &
|
||
restaurer (message clair), tConfig restauré, rappels reprogrammés
|
||
- [ ] **v1.2.7** : prévision après un OUBLI (pas de faux pic dans le passé)
|
||
et après un RETARD (prévision décalée, suivant la dernière prise)
|
||
- [ ] **v1.2.9** : zoom du graphique (pinch 2 doigts + boutons − / + ; labels
|
||
X adaptatifs ; courbes lisses à fort zoom)
|
||
- [ ] **v1.3.3** : **export des logs** → le gestionnaire de fichiers s'ouvre,
|
||
le .txt est écrit, un message « Logs de diagnostic exportés » s'affiche ;
|
||
cas d'échec (annulation du chooser = silencieux, échec d'écriture =
|
||
message d'erreur, l'app ne doit JAMAIS planter — bug #45, Pixel 9 /e/OS)
|
||
- [ ] **v1.3.3** : **version affichée** « 1.3.3 » dans Paramètres et en titre du
|
||
dialog « Nouveautés » (bug #46 : les APK v1.3.0–1.3.2 affichaient 1.3.0) ;
|
||
au premier lancement de v1.3.3, le dialog liste 1.3.1→1.3.3 (dernière vue
|
||
mémorisée = « 1.3.0 » sur les anciens APK)
|
||
- [ ] **v1.3.3** : **événement d'agenda** : le switch demande la permission
|
||
agenda (cette fois le système doit vraiment montrer le dialogue de
|
||
permission — elle est désormais dans le manifest, bug #44) ; après
|
||
accord, l'événement récurrent apparaît dans l'app d'agenda du Pixel 9 ;
|
||
le smoke-test R8 de la checklist §16 doit aussi couvrir export/import
|
||
de backup + graphiques (réflexion Gson sous R8)
|
||
- [ ] **v1.3.4** : **écran Doses** s'ouvre et affiche l'en-tête « temps sous
|
||
THS » avec les données réelles (bug #47 : crash dès que des doses
|
||
antérieures à aujourd'hui existaient — né v1.3.1)
|
||
- [ ] **v1.3.4** : **export des logs** ouvre VRAIMENT le gestionnaire de
|
||
fichiers et le message « Logs de diagnostic exportés » apparaît après
|
||
la sauvegarde (bug #48 : crash au tap depuis v1.3.1) — vérifié sur
|
||
émulateur API 36 (§16.ter), à confirmer au téléphone
|
||
- [ ] **v1.3.4** : rappel de notification affiche la dose proprement
|
||
(lint #49, ex format « 4.0 »)
|
||
- [ ] **v1.3.4** : `./gradlew lint` vert — intégré à la vérification de
|
||
release (step 2 de la checklist §16 désormais : tests + lint)
|
||
- [ ] **v1.3.5** : **agenda end-to-end** : sur un traitement actif avec
|
||
Posologie, activer « Événement d'agenda récurrent » → permission
|
||
accordée → sauvegarder → l'événement existe dans le calendrier local
|
||
« HormoneTrack » (« créé/mis à jour (id=…) » dans les logs exportés) ;
|
||
puis **désactiver le switch → re-save → l'événement disparaît
|
||
RÉELLEMENT** de l'app d'agenda (bugs #50 / #50 bis / #51 : en v1.3.4
|
||
ni création ni suppression ne fonctionnaient, et les re-saves
|
||
orphelinaient les événements — cf §14)
|
||
- [ ] **v1.4.0** : **rappel = jour du créneau uniquement** (fix #52) : un
|
||
traitement avec Posologie 7 j + rappel 18 h ne doit sonner que le jour
|
||
d'injection à 18 h (pas les autres jours) ; la carte « Prochaine
|
||
dose » de l'accueil montre le même créneau ; après « Pris » dans la
|
||
notification, le rappel suivant se cale sur le créneau d'après
|
||
- [ ] **v1.4.0** : **courbes TFS V3C** : superposer Estrannaise/TFS sur un
|
||
traitement EV — les deux doivent être proches aux pics (58–61
|
||
pg/mL/mg) et diverger en queues (V3C = queues exactes) ; vérifier un
|
||
preset EB/EC/PEP (nouvelles courbes) ; PEP à 32,5 mg → niveaux
|
||
comparables à un EEn à 5 mg
|
||
- [ ] **v1.4.1** : **prévision sans saut** : ouvrir Graphiques → activer le
|
||
chip Prévision → le graphique NE BOUGE PAS (avant v1.4.1 il sautait
|
||
« tout à droite ») ; tirer vers la GAUCHE → la courbe projetée défile
|
||
vers le futur (jusqu'à 1 an selon la Posologie) ; « Revenir à
|
||
maintenant » ramène ; désactiver le chip dans le futur → retour auto
|
||
- [ ] **v1.4.1** : **carte « Prochaine dose » en jours** : avec un créneau
|
||
à > 24 h, l'accueil affiche « 5 j 2 h · sam. 6 18:00 (traitement) »
|
||
(et non « 122h22 ») ; à < 24 h, format heures/minutes inchangé
|
||
- [ ] **v1.4.2** : **seuils d'alerte** : Paramètres → « Seuils d'alerte » →
|
||
E2 haut 200 (avec un niveau estimé > 200) → Save → message « Saved » +
|
||
**notification immédiate** (canal dédié) + carte rouge sur l'accueil
|
||
(« ▲ E2 estimé ≈ X — au-dessus de ta limite (200) ») ; re-save sans
|
||
changer → PAS de nouvelle notif (anti-spam) ; seuil haute au-dessus du
|
||
niveau → tout disparaît ; validation : haut ≤ bas → message rouge
|
||
- [ ] **v1.4.2** : **backup avec paramètres** : Export JSON → le fichier
|
||
contient `"settings"` (langue, auto_calibrate, seuils) ; réinstaller +
|
||
Import → langue/seuils restaurés ; un VIEUX backup (v1.4.1) s'importe
|
||
sans les réglages (rétrocompat) ; smoke-test R8 : export/import
|
||
(réflexion Gson sur UserSettings, `-keep` ajouté)
|
||
- [ ] **v1.4.3** : **delta 6 h de l'accueil** : le lendemain d'une injection
|
||
EEn (plateau), la carte « niveau actuel » doit comparer à il y a 6 h
|
||
RÉELLEMENT (bug #53 : avant, la comparaison portait sur 24 h et le
|
||
delta pouvait être trompeusement négatif) — vérifier la cohérence avec
|
||
la courbe 24 h du graphique
|
||
- [ ] **v1.4.4** : **prévision sans saut** : activer le chip ne déplace pas le
|
||
graphique (il s'étend jusqu'à la prochaine dose) ; drag gauche = futur
|
||
jusqu'à 1 an ; « Revenir à maintenant » ; désactivation en plein futur =
|
||
retour auto (fix #54 : avant, le pan futur était figé à 0)
|
||
- [ ] **v1.4.5** : **marqueurs de doses** : ligne pointillée + heure pour les
|
||
doses projetées, triangles discrets pour les prises réelles ; labels X
|
||
à minuit LOCAL du fuseau CHOISI (Paramètres, vide = téléphone) ; la
|
||
prévision ne devait rien à un décalage de génération (créneau exact)
|
||
- [ ] **v1.4.6** : **3ᵉ modèle WHSAH** : preset WHSAH → courbe verte
|
||
superposable aux 2 autres (montée plus rapide à J+1, t½ plus longues
|
||
que TFS) ; PEP non couvert ; 6 presets ; dropdown modèle à 3 choix
|
||
- [ ] **v1.4.7** : **toggles alignés sur les traitements** : au 1ᵉʳ chargement
|
||
du graphique, seuls les modèles des traitements à profil sont ON
|
||
(ex. EV TFS + EEn WHSAH → Estrannaise OFF) ; les autres activables au
|
||
tap ; et TOUTES les courbes affichées reflètent la calibration (test)
|
||
- [ ] **v1.4.8** : **calibration par modèle** : superposer TFS + WHSAH avec
|
||
l'auto-calibration ON → CHAQUE courbe colle aux labs (plus de facteur
|
||
partagé absurde type ×2,21 — fix #60)
|
||
- [ ] **v1.4.9** : **calibration ×2,21 corrigée** (#61) : le bouton
|
||
« Calibrer avec les analyses » et l'auto-calibration retombent sur un
|
||
facteur physiologique (< 1,2) même avec des doses de test anciennes ;
|
||
hint dans l'éditeur quand l'auto-calibration écrase le facteur manuel
|
||
- [ ] **v1.4.10** : **pan sur la vue 24 h** : glisser horizontalement à
|
||
24 h doit déplacer la courbe (fix #62 : les deltas < 1 h s'accumulent
|
||
— avant, le pan ne bougeait jamais sur 24 h) ; tester aussi à 7 j et
|
||
le drag gauche avec prévision (futur)
|
||
- [ ] **v1.5.0** : **Tracé labs** : chip `Tracé labs` (rangée modèles,
|
||
off par défaut) → la courbe rose foncé pointillé passe EXACTEMENT
|
||
sur chacun de tes labs E2 entre le 1er et le dernier LAB ; le reste
|
||
du graphique (toggles modèles, calibration) n'est pas modifié ; les
|
||
jours sans labs récents : la courbe s'arrête au dernier lab (le
|
||
vieillir est normal) ; chip décoché = plus de série ; 0 crash
|
||
- [ ] **v1.6.0** : **Prolonger le tracé labs** : avec `Tracé labs` ON,
|
||
activer `Prolonger` → la courbe continue APRÈS le dernier lab
|
||
(rose atténué) jusqu'à l'extinction du modèle, avec légende dédiée
|
||
ET AVERTISSEMENT « simple simulation, sans garantie… » sous la
|
||
légende ; une dose loguée après le dernier lab fait repartir la
|
||
courbe en pic ; désactiver `Prolonger` → retour à la fenêtre
|
||
[1er ; dernier lab] exacte ; `Tracé labs` OFF → le chip `Prolonger`
|
||
est désactivé ; l'avertissement disparaît avec la série
|
||
- [ ] **v1.7.0** : **auto-backup journalier** : Paramètres → « Sauvegarde
|
||
automatique quotidienne » → activer → le sélecteur de dossier
|
||
s'ouvre → choisir (ex. Download) → un PREMIER `hormonetrack-auto-…json`
|
||
apparaît immédiatement dans le dossier (vérifié : `adb shell ls
|
||
/sdcard/Download/` sur l'émulateur) ; le statut « Dernière
|
||
sauvegarde : OK (date) » s'affiche ; réduction des copies
|
||
(garder 2, injecter des vieux fichiers, re-Save → les plus vieux
|
||
auto-backups partent, JAMAIS les exports manuels) ; désactivation →
|
||
plus de run ; l'export MANUEL reste inchangé
|
||
- [ ] **v1.7.0** : **unités des axes** : « pg/mL » au sommet de l'axe
|
||
gauche (E2) et « ng/mL » au sommet de l'axe droit (T — disparaît
|
||
avec le toggle T), au-dessus des labels numériques ; portées web
|
||
(versions sync)
|
||
- [ ] **v1.7.1** : **notes des analyses** : une prise E2+T avec des notes
|
||
DISTINCTES affiche LES DEUX lignes (« E2 : … » / « T : … ») ;
|
||
note identique sur les deux → une seule ligne ; la note seule reste
|
||
brute ; ré-éditer une entrée → sa note se met à jour sans effacer
|
||
l'autre
|
||
- [ ] **v1.8.0** : **recommandation de prochaine prise de sang** : avec un
|
||
injectable E2 actif à Posologie, la page Analyses affiche la carte
|
||
« Prochaine prise de sang (suggestion) » — creux daté juste avant
|
||
l'injection du créneau associé (+ mention « ou la veille »), statut
|
||
de stabilisation, disclaimer ; l'analyse réellement faite au creux
|
||
recommandé → la carte passe au creux SUIVANT ; enlever la Posologie
|
||
→ carte remplacée par l'invite « renseigne une Posologie » ;
|
||
traitement oral seul → aucune carte
|
||
- [ ] **v1.9.2** : **nuage sans traitement stocké ESE** : avec un
|
||
traitement E2 injectable STOCKÉ en TFS (ou autre), activer Estrannaise
|
||
dans le graphique → la courbe ESE apparaît ; activer Nuage (exclusif
|
||
ESE) → le nuage ENTOURE la courbe ESE tracée sur ces doses (le
|
||
pkModel stocké n'est pas un prérequis) ; l'oral Bateman seul → pas de
|
||
nuage ; l'exclusivité ESE reste sur le chip
|
||
|
||
- [ ] **v1.8.1** : **stabilisation corrigée + suggestion Accueil** : après
|
||
changement de dose/ester/intervalle, la carte ne dit plus « stabilisé
|
||
depuis » l'ancienne période (régime = séquence terminale (ester, dose,
|
||
écart) constants) ; une carte compacte de recommandation apparaît
|
||
aussi sur l'Accueil ; 0 crash
|
||
|
||
---
|
||
*Doc mise à jour le 16 sept. 2026 (v1.7.1) — build OK, lint vert, 211/211 tests verts (181 sans
|
||
les données locales), dépôts Gitea (cloudyfy + farewell) avec releases APK, aucune donnée
|
||
de santé dans le dépôt ni l'historique. Fil des corrections : v1.3.3 = reprise de
|
||
maintenance (permissions agenda, export logs IO, bump de version) ; v1.3.4 = 2 crashs
|
||
reproduits sur émulateur (#47 stringResource arity, #48 LocalDate+pattern horaire), lint
|
||
filet bloquant ; v1.3.5 = diagnostic à distance via les logs exportés (#50/#50bis/#51
|
||
agenda) ; v1.4.0 = modèle TFS V3C sur la méta-analyse officielle (7 esters, fidélité
|
||
~1 % épinglée) + rappels sur la grille Posologie (#52) ; v1.4.1 = prévision étendue sans saut (scroll futur, horizon 1 an) + delta en jours sur l'accueil ; v1.4.2 = seuils d'alerte configurables (carte accueil + notification WorkManager 15 min + anti-spam) et backup JSON v2 avec paramètres ; v1.4.3 = fix du delta 6 h (#53) + régression n°4 (plateau EEn épinglé) ; v1.4.4 = prévision réparée (captures figées #54 + extension de fenêtre) ; v1.4.5 = marqueurs de doses + minuit local + fuseau configurable (#55) + boucle de recomposition tuée (#56) ; v1.4.6 = 3ᵉ modèle PK WHSAH (fit Mona, superposable) + scroll du graphique (#57) ; v1.10.0 = creux pré-injection dans la reco (#68), dialog fantôme tué (#69), rappel sauté si dose déjà loggée. Dette connue : fragments de
|
||
labs dans l'historique git (v1.1.0→v1.2.3) — cf §8.bis.*
|