- §8 : 56 tests, V120FeaturesTest (10) incluant calibration T par ester - §14 : bugs 25-31 (calibration par période, lab T en unité aberrante, commentaires imbriqués dans les KDoc, XML de test périmés, idempotence du scrub d'historique, préfixe « v » des releases) + leçons (f)(g)(h) - §20 : état des faits / reste à faire mis à jour - §21 : checklist des features v1.2.x - README : arbre (scripts/, CHANGELOG, local-test-data gitignoré)
785 lines
49 KiB
Markdown
785 lines
49 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)](#3-stack--versions-épinglées)
|
||
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)
|
||
7. [Moteur pharmacocinétique](#7-moteur-pharmacocinétique)
|
||
8. [Tests unitaires](#8-tests-unitaires)
|
||
9. [Système de rappels](#9-système-de-rappels)
|
||
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)
|
||
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](#16-workflow-build--test--install)
|
||
17. [Montre : Gadgetbridge & options](#17-montre--gadgetbridge--options)
|
||
18. [Espace disque & coûts](#18-espace-disque--coûts)
|
||
19. [Limites connues & choix volontaires](#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).
|
||
|
||
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. |
|
||
| 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 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. |
|
||
|
||
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.2.0)
|
||
|
||
| Composant | Version | Où |
|
||
|---|---|---|
|
||
| Gradle | **9.7.1** (wrapper) | `gradle/wrapper/gradle-wrapper.properties` |
|
||
| AGP | **9.4.0** — **Kotlin intégré** : ne PAS appliquer `org.jetbrains.kotlin.android` ; `kotlinOptions` supprimé (cible JVM via `compileOptions`, 17) | `build.gradle.kts` racine |
|
||
| Kotlin | 2.3.21 (plugin compose 2.3.21) | idem |
|
||
| KSP | 2.3.11 (versionnage indépendant depuis KSP2) | idem |
|
||
| Compose BOM | **2026.08.00** (Compose 1.12 ; material3 pinné par le BOM) | `app/build.gradle.kts` |
|
||
| Room | 2.8.4 (KSP) — **DB v2 + MIGRATION_1_2**, `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 (déclaré, non utilisé — supprimable) | idem |
|
||
| compileSdk / targetSdk | **37** / 37 ; minSdk 26 ; Java target 17 | 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`** ✓
|
||
- **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 2**, 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).
|
||
|
||
### `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`.
|
||
|
||
## 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.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` jusqu'à `toMs`, strictement
|
||
après `nowMs` ; 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 » — renvoie des **copies** de traitements avec les scale factors
|
||
recalculés (médiane lab ÷ prédiction) + TConfig recalibré. Les valeurs stockées ne
|
||
bougent jamais ; 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)
|
||
|
||
**Automatique** (`computeEsterScaleFactors` + `scalePerEster`, option « Auto-calibration ») :
|
||
- 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 /
|
||
batemanParams / concentrationOfDose / computeKa / doseEster / isInjectionRoute`.
|
||
Type de retour : `LevelPoint(timestamp, e2, t)`.
|
||
|
||
## 8. Tests unitaires
|
||
|
||
**56 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.**
|
||
- **`V120FeaturesTest`** (10) : v1.2.0→v1.2.3 — 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.**
|
||
|
||
### 8.bis Données de test réelles : HORS dépôt (`local-test-data/`)
|
||
|
||
Les deux classes de régression (`RegressionUserCase{,2}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` et `backup-v1.2.0.json`
|
||
(copiés tels quels depuis l'export JSON de l'app) ;
|
||
- `.gitignore` contient `local-test-data/` → jamais commités ;
|
||
- 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 44 tests au lieu de 56 ;
|
||
- 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. `git grep` sur **toutes** les
|
||
révisions ne trouve aucune donnée réelle.
|
||
- **`LabsGroupingTest`** (4) : 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).
|
||
- **`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).
|
||
|
||
**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 (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) : plages 24 h/7 j/30 j ; **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**
|
||
- `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 « Fréquence » (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é
|
||
- `SettingsScreen` : langue (Système/Français/English, chips reflétant l'état) ;
|
||
**« 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`)
|
||
- 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 ».
|
||
|
||
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=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`).
|
||
|
||
**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é).
|
||
|
||
**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. (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) :
|
||
|
||
```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 / git
|
||
|
||
```bash
|
||
cd ~/projects/HormoneTrack
|
||
./gradlew assembleDebug testDebugUnitTest # build + 36 tests
|
||
./gradlew lint # linters Android (à configurer)
|
||
adb install -r app/build/outputs/apk/debug/app-debug.apk
|
||
```
|
||
|
||
**Git (initialisé le 2026-09-05, branche `main`, remote Gitea)** :
|
||
- Dépôt : `https://gitea.cloudyfy.fr/Siphonight/HormoneTrack` (privé) ;
|
||
- 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 -u origin main --tags` (auth par trousseau macOS — token Gitea
|
||
à scope `write:repository` ; ⚠️ un token SANS `write:user` ne permet PAS de
|
||
créer un repo via API ni d'utiliser push-to-create, mais suffit pour push,
|
||
créer des releases et y attacher des fichiers).
|
||
|
||
### 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` :
|
||
|
||
```bash
|
||
# 1. Construire l'APK au niveau du tag
|
||
git checkout vX.Y.Z && ./gradlew assembleDebug
|
||
cp app/build/outputs/apk/debug/app-debug.apk /tmp/apks/HormoneTrack-vX.Y.Z.apk
|
||
git checkout main
|
||
|
||
# 2. Créer (ou mettre à jour) la release : corps = CHANGELOG + APK attaché
|
||
python3 scripts/gitea-release.py vX.Y.Z /tmp/apks/HormoneTrack-vX.Y.Z.apk
|
||
```
|
||
|
||
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) ;
|
||
- 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).
|
||
|
||
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 possible sur demande ;
|
||
**les tests humains sur vrai téléphone restent la référence**
|
||
(notifs → montre, UX de saisie, pickers, panoramique du chart)
|
||
|
||
## 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 = 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. Verrou biométrique (BiometricPrompt), widget, export CSV
|
||
6. Charts : **zoom** + tooltip au toucher (le pan est fait, v1.2.0) ;
|
||
MaterialExpressiveTheme quand l'API passera publique (cf §3)
|
||
7. Release workflow : version signée (release build) au lieu de debug APK
|
||
|
||
Fait (à ne pas refaire) : pan du chart (v1.2.0), 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), dépôt Gitea
|
||
+ releases APK (push session).
|
||
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)
|
||
- [ ] **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 Fréquence 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)
|
||
|
||
---
|
||
*Doc mise à jour le 5 sept. 2026 (v1.2.3) — build OK, 56/56 tests verts (44 sans les données locales), dépôt Gitea privé + releases APK, aucune donnée de santé dans le dépôt ni l'historique.*
|