# 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 : `/Users/siphonight/owncloud/Shared_Spreadsheets/Estrogen.ods` (30 MB, 13 tables) - Tables nominatives (« Jenny », « Océane », « Elden », « Alice », « Marie », « Dana ») : historique injections (datetime, cuisse L/R, ester, dose mg, Z-track) + labs (E2 pg/mL, T ng/mL) + **facteur d'échelle** manuel (Jenny 0,9 ; Océane 0,55 ; Elden 0,72 ; Alice 0,5) - 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?` — 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 **54 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`** (8) : v1.2.0/v1.2.1 — 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`). **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) : ```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. 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 (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.*