Bug remonté : activer Estrannaise dans le graphique puis le nuage n'affichait rien tant qu'aucun traitement n'était STOCKÉ avec le modèle ESE — « la seule méthode trouvée était de mettre un traitement en cours sur le modèle ESE, mais ça ne devrait pas être un prérequis ». Cause : le filtre du nuage testait le pkModel STOCKÉ du traitement, alors que la courbe ESE affichée redessine toutes les doses E2 avec modelOverride = "ESE" (peu importe le modèle stocké) — courbe et nuage n'utilisaient pas la même définition de « quelles doses sont tracées en ESE ». Fix : EstrannaiseCloud.compute couvre toutes les doses E2 à profil injectable dont l'ESTER EFFECTIF (override compris) est couvert par le fit Estrannaise — indépendamment du modèle stocké. L'exclusivité ESE reste portée par le chip (activable seulement si ESE est affiché). L'oral Bateman reste hors nuage (pas d'ester échantillonnable). Les traitements inactifs sont inclus (§6.bis : la courbe ESE les trace aussi). Tests réécrits (237 verts / 207 sans données locales) : « les doses d'un traitement TFS sont couvertes quand ESE est affiché » (épinglé Android + web) ; oral seul → vide (conservé). Validé émulateur §16.ter sur le profil réel de l'utilisatrice (EEn stocké TFS, ESE affiché, nuage visible autour de la courbe, 0 crash). Web miroir v1.9.2. versionCode 40 / versionName 1.9.2.
2327 lines
175 KiB
Markdown
2327 lines
175 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.8.1)](#3-stack--versions-épinglées--à-jour-v181)
|
||
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)
|
||
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.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 |
|
||
|---|---|
|
||
| 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.8.1)
|
||
|
||
| 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) | 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 `PKProfileStore` (asset), canal de notification
|
||
- `MainActivity` : applique la langue sauvegardée (`AppCompatDelegate.setApplicationLocales`),
|
||
demande POST_NOTIFICATIONS (API 33+), lit les extras d'intent `open_log_dose` +
|
||
`treatment_id` (venus de la notification) → Home pré-ouvre le dialog de log
|
||
- **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 |
|
||
| ❌ 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` (asset loader + échantillonnage)
|
||
|
||
- Asset `app/src/main/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)` lit l'asset
|
||
- `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` = longueur de table (8001 h) pour les profils,
|
||
`30 × t½` pour Bateman.
|
||
|
||
### 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 est
|
||
constant** (= l'écart des deux doses les plus récentes, comparé
|
||
exactement — 7 j ± 1 h casse le régime). 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** : minimum local de la courbe E2 prévisionnelle (doses réelles +
|
||
[PharmacokineticEngine.generateForecastDoses], pas 1 h) entre deux
|
||
injections ; 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é) ; créneau ≥ stabilisation (sinon creux
|
||
du créneau suivant — décision v1.8.0 « sauter »).
|
||
- **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.
|
||
- 11 tests `LabTimingTest` (8 + 3 v1.8.1 régime récent) + 1 test régression n°5.
|
||
|
||
### 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`.
|
||
|
||
### 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/ — modèle 3
|
||
compartiments 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 : « 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.
|
||
- **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 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. Limites : les
|
||
contributions TFS/WHSAH ne sont pas dans le nuage (pas de posterior
|
||
publié) ; 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`.
|
||
|
||
## 8. Tests unitaires
|
||
|
||
**234 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — **193 sans
|
||
les données de test locales** (cf §8.bis : les 5 classes de régression,
|
||
6/6/6/5/7 tests, 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.
|
||
- **`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` et `backup-v1.7.0.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) ;
|
||
- `.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 193 tests au lieu de 223 (v1.8.1 ; 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) ;
|
||
- 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 envisageable : Robolectric
|
||
(UI/logic Android), tests Compose, lint.
|
||
|
||
## 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)
|
||
- `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** ;
|
||
**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
|
||
- `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` : 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 ; à propos
|
||
- 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** (223 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`).
|
||
|
||
## 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 + 223 tests (193 sans données locales)
|
||
./gradlew assembleRelease # APK optimisé R8 (cf §16.bis)
|
||
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)** : `./gradlew assembleRelease` — R8 full mode +
|
||
`isShrinkResources` + signé avec la **clé debug** (même signature que les APK
|
||
debug distribués → mise à jour par-dessus sans perte de données ; 20 Mo →
|
||
2,4 Mo). 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).
|
||
|
||
### 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` — 223 au
|
||
total, 193 si `local-test-data/` est absent (les 5 classes de régression
|
||
réelles sont skippées via `Assume`, 6/6/6/5/7 tests) ; **lint vert
|
||
obligatoire** : `./gradlew lint` (v1.3.4 — `StringFormatMatches` aurait
|
||
attrapé les crashs #47/#48 dès v1.3.1). Paliers de test du projet :
|
||
chaque commit = tests + lint ; release = + 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 223 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. 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)
|
||
```
|
||
|
||
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 ;
|
||
signé clé debug → upgradable sans perte, mais pas une signature « officielle »
|
||
- **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 + tests Compose** (VM Android en JVM — pas besoin d'appareil)
|
||
2. **Émulateur local** pour smoke-tests UI (sur demande, ~2–3 Go)
|
||
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** (signature dédiée ≠ clé debug → nécessite
|
||
une migration : backup → désinstallation → installation signée → réimport)
|
||
|
||
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).
|
||
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** :
|
||
les deux `runBlocking` séquentiels peuvent fusionner trivialement ; le
|
||
passage complet au pattern `goAsync`+coroutine de ReminderReceiver est
|
||
équivalent mais re-testé sur émulateur (boot). Différé.
|
||
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.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). Dette connue : fragments de
|
||
labs dans l'historique git (v1.1.0→v1.2.3) — cf §8.bis.*
|