HormoneTrack/docs/DEVELOPPEMENT.md
Siphonight 8f7a235c93 v1.1.0 : suivi THS avec courbes E2/T, modèles Estrannaise & Transfem Science, calibration labs, rappels (GT 3), backup JSON, FR/EN
- Moteur PK par tables horaires (8001 h) extraites de Estrogen.ods + Bateman fallback
- Lookup profils insensible à la casse (fix courbes EEn plates)
- Conversion unités T (ng/dL, ng/L, nmol/L -> ng/mL)
- Édition des doses + override d'ester par injection
- 30 tests unitaires dont régression sur données réelles
- UI Compose Material 3, i18n FR/EN, sauvegarde JSON SAF
2026-09-05 16:32:44 +02:00

30 KiB
Raw Blame History

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, GUIDE_INSTALLATION.md, MONTRE-GADGETBRIDGE.md.


Table des matières

  1. Contexte & objectifs
  2. Historique du projet
  3. Stack & versions (épinglées)
  4. Environnement de build (cette machine)
  5. Architecture générale
  6. Modèle de données (Room)
  7. Moteur pharmacocinétique
  8. Tests unitaires
  9. Système de rappels
  10. UI & navigation
  11. Graphiques (CurveChart)
  12. i18n FR/EN
  13. Sauvegarde JSON
  14. Bugs corrigés (historique complet — à ne pas réintroduire)
  15. Comment régénérer l'asset pk_profiles.json
  16. Workflow build / test / install
  17. Montre : Gadgetbridge & options
  18. Espace disque & coûts
  19. Limites connues & choix volontaires
  20. Idées d'évolution (Phase 2+)
  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).

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
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.

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)

Composant Version Où
Gradle 8.9 (wrapper jar v8.9.0) gradle/wrapper/
AGP 8.5.2 build.gradle.kts racine
Kotlin 2.0.0 + plugin compose 2.0.0 idem
KSP 2.0.0-1.0.21 idem
Compose BOM 2024.06.00 app/build.gradle.kts
Room 2.6.1 (KSP) idem
Navigation Compose 2.7.7 idem
AppCompat 1.7.0 (langue par app) idem
DataStore Preferences 1.1.1 idem
Gson 2.11.0 idem
JUnit 4.13.2 (testImplementation) idem
WorkManager 2.9.1 (déclaré, non utilisé — supprimable) idem
compileSdk/targetSdk 34 ; minSdk 26 ; Java target 17 app

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é.

4. Environnement de build (cette machine)

  • macOS (Apple Silicon), brew présent, Java 21 (Microsoft OpenJDK) sur /usr/bin/java ✓
  • SDK Android : installé via brew install --cask android-commandlinetools → /opt/homebrew/share/android-commandlinetools (524 MB)
    • licences acceptées : yes | sdkmanager --licenses
    • paquets : platform-tools, platforms;android-34, build-tools;34.0.0
  • local.properties à la racine (non commité) : sdk.dir=/opt/homebrew/share/android-commandlinetools
  • Gradle 8.9 téléchargé par le wrapper ; caches ~/.gradle ≈ 1,5 GB
  • Build validé : ./gradlew assembleDebug testDebugUnitTest → BUILD SUCCESSFUL, APK debug 18 MB (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 1, fallbackToDestructiveMigration() (⚠️ à retirer avant toute migration réelle — sinon perte de données silencieuse).

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)
  • 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.

7. Moteur pharmacocinétique

pk/PharmacokineticEngine.kt + pk/PKProfileStore.kt.

7.1 Source : Estrogen.ods

  • Fichier : Estrogen.ods de l'utilisatrice (Owncloud) (30 MB, 13 tables)
  • Tables nominatives (6 profils (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 (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.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·E2(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. Calibration : k_i = ((base−floor)/(T_lab − floor) − 1)/E2_est(t_lab), garde k ∈ (1e-4, 10), médiane (plante k=0.25 → recalibre 0.25 ±15 %, testé).

7.6 Calibration E2 (par traitement)

computeScaleFactor(treatment, allDoseLogs, e2Labs) : ratio_i = lab.value / Σ contributions du traitement seul (scale=1) à t_lab → médiane des ratios (garde : prédiction > 0.5 pg/mL), arrondi 2 décimales. C'est l'automatisation de la colonne « Scale factor » du .ods. Déclenchable depuis l'éditeur de traitement ; valeur éditable manuellement.

7.7 API du moteur

levelAt / currentLevel / computeCurve(start, end, step=1h, tConfig) / e2At / testosteroneAt / computeScaleFactor / computeTConfigCalibration / nextReminderFireMs / batemanParams / concentrationOfDose / computeKa / doseEster / isInjectionRoute. Type de retour : LevelPoint(timestamp, e2, t).

8. Tests unitaires

30 tests JVM, tous verts (./gradlew testDebugUnitTest). 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é
  • BackupGsonTest (1) : round-trip JSON complet (enums, IDs, notes, TConfig)
  • 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.

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).

  • ReminderContract : constantes + fabrique unique reminderIntent() pour schedule ET cancel (même action = même PendingIntent — cf bug §14.3)
  • AlarmScheduler :
    • quotidien : setExactAndAllowWhileIdle si canScheduleExact() (API≥31 : alarmManager.canScheduleExactAlarms()), sinon setWindow ±10 min
    • permission SCHEDULE_EXACT_ALARM : bouton d'octroi dans Paramètres + éditeur (Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM)
    • scheduleDaily (prochaine occurrence HH:mm), scheduleSnooze (+1 h), rescheduleAll
  • 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()
  • DoseActionReceiver (non exporté) : « Pris » → goAsync() + coroutine IO → insert DoseLog (dose = extra ou standard) ; « Reporter 1 h » → scheduleSnooze ; annule la notif
  • BootReceiver : goAsync() + thread + runBlocking + one-shot getActiveOnce() (jamais un Flow en runBlocking !) → reschedule

Manifest : POST_NOTIFICATIONS, SCHEDULE_EXACT_ALARM, RECEIVE_BOOT_COMPLETED, VIBRATE. Sur la montre : remontée par Gadgetbridge ou Huawei Health (cf §17).

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), carte prochaine dose, chips de log rapide (+ FAB), mini-chart 24 h, disclaimer ; rafraîchissement tick 60 s
  • ChartScreen : 24 h/7 j/30 j, toggles T + labs, chart 320 dp + légende
  • DosesScreen : LazyColumn par jour (desc), suppression avec confirmation, FAB → LogDoseDialog (traitement, dose, DateTimeField, notes)
  • LabsScreen : groupée par marqueur, FAB → LabDialog (E2/T/PRL, unité suggérée)
  • 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 ») ; carte Rappel (switch + TimePicker + avertissement alarmes exactes) ; switch actif ; save → insert/update + schedule/cancel ; delete avec confirmation ; createdAt préservé à l'édition
  • SettingsScreen : langue (Système/Français/English, chips reflétant l'état) ; réglages T + calibration ; statut alarmes exactes ; Export/Import JSON ; à propos + crédits
  • Composants : CurveChart, DateTimeField (DatePicker+TimePicker Material3, LocalDateTime), LogDoseDialog, LabDialog, formatDose() (top-level réutilisée)
  • Thème M3 custom (ui/theme/Color.kt : bleu #4F5BD5, rose #D6589E, labs orange, bandeau TransSky/TransPink), dynamic color désactivé
  • ⚠️ Card(onClick=…) et ExposedDropdownMenuBox = API expérimentales M3 → @OptIn requis sur chaque composable qui les utilise

11. Graphiques (CurveChart)

Canvas pur (aucune lib). Dual axe : E2 gauche (pg/mL), T droite (ng/mL, pointillés rose). Échelle « nice » (niceCeil : 1/2/2.5/5/10 × 10ⁿ). 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. Ligne verticale « maintenant ».

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)

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=1, exportedAt, treatments[], doseLogs[], labResults[], tConfig} → Gson
  • Les IDs Room sont conservés dans l'export et réinsérés tels quels → les FK dose→traitement restent valides
  • Import = ajout (traitements → doses → labs) ; ré-import du même fichier → conflit d'ID unique → exception catchée → import_fail (voulu ; un mode « replace » est en §20)
  • Transport : SAF (CreateDocument("application/json") / OpenDocument), écriture openOutputStream(uri, "wt") ; ⚠️ pas de return dans un expression body = try{}

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).

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 (RegressionUserCaseTest = export JSON réel) attrape les bugs de convention (casse, unités) que les tests synthétiques ratent ; (e) attention aux identifiants « presque pareils » entre sources (constantes app vs clés d'asset).

15. Comment régénérer l'asset pk_profiles.json

Si le .ods change (re-fits, nouveaux esters) :

# 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

cd ~/projects/HormoneTrack
./gradlew assembleDebug testDebugUnitTest     # build + 24 tests
./gradlew lint                                # linters Android (à configurer)
adb install -r app/build/outputs/apk/debug/app-debug.apk
  • Téléphone : mode développeur + Débogage USB (détails : GUIDE_INSTALLATION.md)
  • À ma charge (assistant) : build + tests JVM ✓ ; émulateur possible sur demande (~2–3 Go + image système) ; les tests humains sur vrai téléphone restent la référence (notifs → montre, UX de saisie, pickers)
  • Recommandé avant chaque commit : ./gradlew testDebugUnitTest (les tests du moteur attrapent les régressions mathématiques)

17. Montre : Gadgetbridge & options

Doc dédiée : 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) :

Élément Taille
SDK Android (cmdline-tools + platform 34 + build-tools + platform-tools) 524 MB
Cache Gradle (~/.gradle) 1,5 GB
Projet (sources + build outputs) 69 MB
Total outillage actuel ≈ 2,1 GB

Marges : émulateur + image système ≈ +2–3 GB ; DevEco Studio (Phase 2) ≈ +10 GB → tout rentre très largement. Aucune contrainte disque prévue.

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 = ajout seulement (pas de mode replace/dédup)
  • fallbackToDestructiveMigration() — à retirer à la migration v2 du schéma
  • WorkManager déclaré non utilisé
  • 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 → seul backup = export JSON manuel

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 : mode replace (wipe + insert) + détection de doublons
  5. Migration Room v2 (retirer fallbackToDestructiveMigration)
  6. Verrou biométrique (BiometricPrompt), widget, export CSV
  7. Charts : zoom/pan + tooltip au toucher
  8. Phase 2 montre : watchface .hwt custom, puis mini-app Lite Wearable (cf §17)
  9. Retirer WorkManager ou l'utiliser (reschedule de sécurité quotidien)

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)

Doc mise à jour le 5 sept. 2026 — build OK, 24/24 tests verts, APK debug 18 MB.