1547 lines
108 KiB
Markdown
1547 lines
108 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) + [6.bis Sémantique isActive](#6bis-sémantique-isactive-v124--drapeau-administratif-jamais-un-filtre)
|
||
7. [Moteur pharmacocinétique](#7-moteur-pharmacocinétique)
|
||
8. [Tests unitaires](#8-tests-unitaires) + [8.bis Données de test hors dépôt](#8bis-données-de-test-réelles--hors-dépôt-local-test-data)
|
||
9. [Système de rappels](#9-système-de-rappels)
|
||
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) + [16.bis Releases Gitea](#16bis-releases-gitea-avec-apk-téléchargeable)
|
||
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).
|
||
|
||
> **Développement IA-assisté** : le code a été produit avec un assistant IA ;
|
||
> la contribution humaine = feedback continu, retours utilisateur (tests réels
|
||
> sur téléphone, bugs avec exports), suggestions et validation des releases
|
||
> (détail §2 — factuel, session par session).
|
||
|
||
Fonctionnalités v1 :
|
||
- Courbes estimées heure par heure : E2 (pg/mL) + T (ng/mL) — 24 h / 7 j / 30 j
|
||
- Deux modèles PK du `.ods` (Estrannaise / TFS) pour injections EV/EU/EEn ;
|
||
Bateman paramétrable pour gel/patch/oral
|
||
- Log des doses (date/heure exacte, mg, **ester par injection**)
|
||
- Labs (E2/T/PRL) + **calibration** (facteur d'échelle + calibration k du modèle T)
|
||
- Rappels quotidiens, actions « Pris » / « Reporter 1 h » dans la notification
|
||
- Export/Import JSON, FR/EN, 100 % local
|
||
|
||
## 2. Historique du projet
|
||
|
||
| Date | Événement |
|
||
|---|---|
|
||
| 5 sept. 2026 (session 1) | Plan, vérification GT 3 = Lite Wearable, création couche données + ancien moteur Bateman + ancien ReminderManager. Extraction des modèles du `Estrogen.ods` → `/tmp/pk_models.json` (6 profils × 8001 h + params D/k1–k3). |
|
||
| 5 sept. 2026 | L'utilisatrice mentionne un travail d'un assistant tiers « **Mimo V2.5** » : **aucune trace trouvée** (fichiers identiques à la session 1, timestamps identiques). Reprise depuis l'état existant. Bugs trouvés au passage : settings.gradle, BootReceiver, cancel PendingIntent. |
|
||
| 5 sept. 2026 (session build) | Redesign données (ester/pkModel/scaleFactor), réécriture moteur PK sur tables ODS, modèle T + calibration, rappels complets, UI 6 écrans, chart Canvas, backup JSON, i18n, wrapper Gradle, guide. **Installation SDK Android (brew) + premier build.** |
|
||
| 5 sept. 2026 (session tests/docs) | Correction de toutes les erreurs de compilation (dont 3 vrais bugs logiciels trouvés par les tests), **24 tests unitaires verts**, APK debug généré (18 MB), documentation complète (README + docs/), préparation repo git. |
|
||
| 5 sept. 2026 (session v1.1.0) | Bugs remontés par l'utilisatrice : courbes vides (casse EEn) + pas d'édition des doses → corrigés ; régression épinglée sur ses données réelles ; APK v1.1.0. |
|
||
| 5 sept. 2026 (session v1.2.0) | **Repo git initialisé** (commits par couche + tags de release) ; montée toolchain AGP 9.4/Gradle 9.7.1/Kotlin 2.3.21/BOM 2026.08.00/compileSdk 37 ; panoramique du chart, superposition des deux modèles, prévision par « Fréquence », calibration automatique optionnelle, intervalles entre doses, TimePicker centré ; **36 tests verts**, APK v1.2.0 (23 MB). |
|
||
| 5 sept. 2026 (session builds release) | Question debug vs release → **builds release optimisés** (R8 full mode + shrinkResources, 20 Mo → **2,4 Mo**, signés avec la clé debug → upgradables sans perte) ; garde-fous Gson dans `proguard-rules.pro` (réflexion) ; releases publiées avec les **deux APK** (release recommandé + debug) ; 62 tests verts. |
|
||
| 5 sept. 2026 (session v1.2.5) | Bug graphique : double espace en haut (double insets) → **edge-to-edge propre** (`enableEdgeToEdge` + insets consommés une seule fois) ; 62 tests verts, APK v1.2.5 + release. |
|
||
| 5 sept. 2026 (session v1.2.4) | Bug `isActive` (simulation effacée) → sémantique « drapeau administratif » (§6.bis) ; régression n°3 sur le 3ᵉ export réel (transition EV inactif → EEn actif, 38 doses, 22 labs) ; **62 tests verts**, APK v1.2.4. |
|
||
| 5 sept. 2026 (session push Gitea + v1.2.3) | Push initial vers **gitea.cloudyfy.fr/Siphonight/HormoneTrack** (privé) après **anonymisation de l'historique** (filter-branch : les premiers commits embarquaient les valeurs réelles des tests) ; **données de test réelles déplacées hors dépôt** (`local-test-data/` gitignoré, tests `Assume`-skippés) ; **releases avec APK en pièce jointe** ; pics/creux + calibration T par période d'ester ; **56 tests verts**. |
|
||
| 5 sept. 2026 (session v1.2.1) | Remontées UX/utilisateur : calibration **par période d'ester** (labs EV → doses EV, labs EEn → doses EEn — corrige les courbes gonflées), lab T "pg/mL" neutralisé, **édition des labs**, tap accueil → Graphiques + mini-légende, auto-cal fusionnée (E2+T, un seul switch) ; 2ᵉ régression épinglée sur le nouvel export ; **44 tests verts**, APK v1.2.1. |
|
||
| 5 sept. 2026 (session v1.2.2) | Toggle T masque aussi les labs T ; **prise de sang E2 + T en une entrée** (chacune optionnelle) affichée **côte à côte** ; sélecteur d'édition par marqueur ; suppression par prise ; 4 tests de regroupement ; **48 tests verts**, APK v1.2.2. |
|
||
| 5 sept. 2026 (session v1.2.6) | Valeurs estimées affichées sur les pics/creux (drawExtremum) ; **import JSON en mode écrasement** (bug : conflit d'IDs si données existantes) avec restauration du tConfig et reprogrammation des rappels ; docs ; 62 tests verts, APK v1.2.6 + releases. |
|
||
| 6 sept. 2026 (session v1.3.1) | En-tête « temps sous THS » sur Doses ; **logs de diagnostic** (AppLog : buffer 500 lignes, persisté, exportable de Paramètres) ; **fix toggle agenda** (callback async) ; **87 tests verts**, APK v1.3.1 + releases. |
|
||
| 6 sept. 2026 (session v1.3.2) | 3 bugs remontés : toggle agenda (callback async fix + ContextCompat au save + Posologie requise), export logs plantait (pattern JSON réutilisé + AppLog), dialog changelog récurrent (version vue mémorisée avant affichage + titre BuildConfig documenté) ; export réel mis à jour (3 traitements : EV inactif + EEn actif + CPA oral) ; **87 tests verts**, APK v1.3.2 + releases. |
|
||
| 6 sept. 2026 (session farewell) | Repo créé côté farewell → **push SSH (alias `farewell` : giteassh:2222) + 12 releases publiées avec APK vérifiés par téléchargement** ; piège lexicographique `v1.2.10 < v1.2.5` en comparaison de chaînes épinglé (§14 #38) ; les deux instances Gitea sont synchrones. |
|
||
| 6 sept. 2026 (session v1.3.3) | **Audit de reprise de maintenance** (nouvelle session IA) : 3 bugs racines trouvés — permissions agenda ABSENTES du manifest (jamais déclarées, §14 #44), export logs plantait TOUJOURS (le fix v1.3.2 réimplémentait l'IO au lieu de réutiliser BackupManager, §14 #45), bump de version JAMAIS commité (tags v1.3.0–1.3.2 tous `versionCode 14 / "1.3.0"`, §14 #46). Corrigés + doc rafraîchie (DB v3, targetSdk 36, régression 3 = export v1.3.1, §19/§20 staleness) ; **téléphone de test documenté : Google Pixel 9 /e/OS** (AOSP ; SAF DocumentsUI standard). |
|
||
| 6 sept. 2026 (session v1.3.4) | **Deux crashs v1.3.3 remontés (Doses + export logs)** → reproduits sur ÉMULATEUR avec les vraies données (recette §16.ter, désormais standard) : (a) Doses = string `hrt_duration` 3 placeholders vs 2 args (**né en v1.3.1**, §14 #47) ; (b) export logs = `LocalDate.format("…-HHmm")` levant `UnsupportedTemporalTypeException` au TAP (§14 #48) — les fixes IO v1.3.2/v1.3.3 étaient à côté du vrai problème. Lint mis en filet bloquant (aurait attrapé les deux) + #49 notif ; helper `ExportFileNames` + 3 tests (90 verts) ; fixes VÉRIFIÉS sur émulateur avec l'APK release + données réelles (flux SAF complet : SAVE → fichier + message). Publication v1.3.4. |
|
||
| 6 sept. 2026 (session v1.3.5) | **Premier diagnostic à distance RÉUSSI via les logs exportés** : les AppLog de la v1.3.4 prouvent export logs `ok=true` (fix #48 ✓ au téléphone), permission agenda accordée (fix manifest ✓) et révèlent le bug racine EXPLICITE : `ensureCalendar` plantait car l'URI sync-adapter n'embarquait pas ACCOUNT_NAME/ACCOUNT_TYPE (§14 #50). En validant sur émulateur, DEUX bugs découverts : delete d'event sans account (#50 bis — « supprimé » logué mais l'event restait) et `buildTreatment` sans `calendarEventId` (#51 — événements orphelins). Cycle ON→save→OFF→save validé end-to-end (`content query`). Publication v1.3.5. |
|
||
| 6 sept. 2026 (session v1.4.0) | **Modèle TFS reconstruit sur la méta-analyse officielle** (params V3C du simulateur transfemscience.github.io/injectable-e2-simulator, extraits de `ester-data.js`/`calc-curve.js`) : forme close 3 compartiments, 7 esters (EB/EC/ECS/PEP ajoutés), pics de l'article reproduits à ~1 % (AUC + Figure 11 épinglés). **+ Fix #52** (remontée) : rappels quotidiens même hors jour d'injection → grille Posologie (moteur = source unique), re-programmation après notif et « Pris ». 12 nouveaux tests (102 verts), lint vert. Publication v1.4.0. |
|
||
| 6 sept. 2026 (session v1.4.1) | **UX graphique et carte « Prochaine dose »** (remontées) : (a) activer la Prévision ne saute PLUS dans le futur — la fenêtre reste en place, la projection s'étend au-delà et se parcourt en tirant vers la gauche (panHours NÉGATIF = futur, clamp unique `clampPanHours`, horizon 12× Posologie borné 30 j–1 an) ; (b) delta en JOURS sur la carte « Prochaine dose » au-delà de 24 h (`HrtDuration.daysAndHours` + `next_dose_days` FR/EN). 5 tests (112 verts). Publication v1.4.1. |
|
||
| 6 sept. 2026 (session v1.4.2) | **Seuils d'alerte configurables + notification** (demande) : limites E2/T hautes/basses (Paramètres, opt-in, validées haut > bas), cartes d'avertissement sur l'accueil (niveau ESTIMÉ actuel), **notification** via un worker WorkManager périodique 15 min (enfin utilisé !) + check one-time au save, anti-spam par état persisté (`Alerts.encodeState`/`shouldNotify`), canal dédié `hormonetrack_alerts`. + **backup JSON v2** : les PARAMÈTRES utilisateur (langue, auto-cal, seuils) voyagent dans l'export et sont restaurés à l'import (rétrocompat v1, `-keep` R8 pour UserSettings). 9 tests (129 verts). Publication v1.4.2. |
|
||
| 7 sept. 2026 (session v1.4.3) | **Question pharmacocinétique → 1 bug + 1 confirmation** : « l'estimation ne remonte pas le lendemain de mon injection EEn » → (a) **comportement NORMAL** (contribution J+1 < 12 % du total, plateau d'équilibre — épinglé par la régression n°4 sur le 4ᵉ export réel) ; (b) **bug #53** : le delta « vs il y a 6 h » de l'accueil comparait en réalité il y a 24 h (`firstOrNull` sur la fenêtre 24 h) → helper `pointHoursBefore` + 4 tests. 137 verts. Publication v1.4.3. |
|
||
| 6 sept. 2026 (session v1.3.0) | Dialog « Nouveautés » après mise à jour (CHANGELOG embarqué en asset, tâche `copyChangelog`, version vue en DataStore) ; **événements d'agenda récurrents** (calendrier local HormoneTrack, RRULE posologie, permission runtime, Room v3 `calendarEventId`) ; Paramètres : version + lien releases ; **78 tests verts**, APK v1.3.0 + releases. |
|
||
| 5 sept. 2026 (session v1.2.10) | Sens des boutons de zoom inversé (+ = zoom avant, convention carte — retour utilisateur) ; 67 tests verts, APK v1.2.10 + releases. |
|
||
| 5 sept. 2026 (session v1.2.9) | **Zoom du graphique** (pinch + boutons, 6 h → 300 j, focal stable, échantillonnage adaptatif `stepForRange`, labels X 1 h/3 h) ; **README : disclaimer IA-assisté** en en-tête ; 3 tests ; **67 tests verts**, APK v1.2.9 + releases. |
|
||
| 5 sept. 2026 (session v1.2.8) | « Fréquence d'injection » → **« Posologie »** (terme adapté à toutes les voies) ; quirk Gitea découvert (noms d'assets normalisés à l'upload) → renommage PATCH dans le script ; releases avec les 2 APK ; 64 tests verts. |
|
||
| 5 sept. 2026 (session v1.2.7) | Prévision : les créneaux passés ne sont plus simulés (bug oubli → faux pic historique) ; retard = décalage voulu ; mismatch doc-code §7.3b corrigé ; 2 tests ; **64 tests verts**, APK v1.2.7 + releases. |
|
||
|
||
Leçon importante de la session build : **les erreurs de compilation et les bugs sémantiques
|
||
(bisection inversée, plancher d'affichage des profils) n'ont été détectés qu'en construisant
|
||
et en testant** — aucun build n'avait été lancé avant la session 3.
|
||
|
||
## 3. Stack & versions (épinglées — à jour v1.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 v3 + MIGRATION_1_2 (forecastIntervalDays) + MIGRATION_2_3 (calendarEventId)**, `fallbackToDestructiveMigration` retiré | idem + `AppDatabase.kt` |
|
||
| Navigation Compose | 2.10.0 | idem |
|
||
| AppCompat | 1.8.0 (langue par app) | idem |
|
||
| DataStore Preferences | 1.2.1 | idem |
|
||
| Gson | 2.14.0 | idem |
|
||
| JUnit | 4.13.2 (testImplementation) | idem |
|
||
| WorkManager | 2.11.2 (**UTILISÉ depuis v1.4.2** : worker périodique des seuils d'alerte, cf §9.bis) | idem |
|
||
| compileSdk / targetSdk | **37** / **36** ; minSdk 26 ; Java target 17 | app |
|
||
| buildFeatures | compose + **buildConfig** (VERSION_NAME pour l'app) | app |
|
||
|
||
Notes importantes (v1.2.0) :
|
||
- **AGP 9** : Kotlin est intégré à AGP — appliquer `org.jetbrains.kotlin.android` est une
|
||
erreur ; le plugin `org.jetbrains.kotlin.plugin.compose` reste appliqué normalement.
|
||
- **compileSdk 37** : la plateforme `platforms;android-37` n'était pas dans sdkmanager
|
||
(API 37 en preview à la date du build) mais **AGP l'a auto-téléchargée** (licences
|
||
signées) — le build passe.
|
||
- **Compose 1.12 (BOM 2026.08.00) exige compileSdk ≥ 37 et AGP ≥ 9.1** ; le BOM
|
||
2026.06.01 est le dernier compatible compileSdk 36.
|
||
- **Material Expressive** : `MaterialExpressiveTheme` / `ExperimentalMaterial3ExpressiveApi`
|
||
sont encore **internal** dans la ligne material3 pinnée par ce BOM (erreur de
|
||
compilation vérifiée — javap montre `public` JVM mais la visibilité **Kotlin** est
|
||
internal). `MaterialTheme` standard conservé ; basculer dès que l'API devient
|
||
publique (NOTE dans `ui/theme/Theme.kt`).
|
||
- Kotlin 2.0 → compose compiler via `org.jetbrains.kotlin.plugin.compose`. Room convertit
|
||
les enums ↔ String automatiquement. **Ne pas monter Kotlin/AGP/Gradle sans vérifier la
|
||
matrice de compatibilité** (les versions sont récupérées via maven-metadata.xml de
|
||
dl.google.com / repo1.maven.org, pas devinées).
|
||
|
||
## 4. Environnement de build (cette machine)
|
||
|
||
- **macOS (Apple Silicon), brew présent, Java 21 (Microsoft OpenJDK) sur `/usr/bin/java`** ✓
|
||
- **Téléphone de test** : **Google Pixel 9 sous /e/OS** (ROM dé-Googlée, base AOSP) —
|
||
SAF/DocumentsUI standard (l'export JSON via `CreateDocument` y fonctionne → c'est LE
|
||
pattern d'IO de référence pour tout export de fichier) ; pas de Play Store, sideload
|
||
par `adb install` ou APK direct. Les retours utilisateur (bugs, exports JSON) viennent
|
||
de ce téléphone. (Historique : la montre est une Huawei Watch GT 3 via Gadgetbridge,
|
||
cf §17 — ce n'est PAS le téléphone.)
|
||
- **Émulateur local (installé en v1.3.4)** : SDK brew =
|
||
`/opt/homebrew/share/android-commandlinetools`. Packages installés :
|
||
`emulator` 37.1.11 + `system-images;android-31;aosp_atd;arm64-v8a` (ATD : léger,
|
||
boot rapide, MAIS DocumentsUI STUBBÉ (`fakesystemapp`) → SAF non pilotable) et
|
||
`system-images;android-36;google_apis;arm64-v8a` (image complète — REQUISE pour
|
||
tester les flux SAF). AVD `hrt` (ATD 31) et `hrt36` (API 36) créés via
|
||
avdmanager (erreur béninie « devices.xml » au create : l'AVD est créé quand même).
|
||
Boot headless : `emulator -avd hrt36 -no-window -no-audio -no-boot-anim -no-snapshot
|
||
-gpu swiftshader_indirect`. Recette de test → **§16.ter**.
|
||
- **SDK Android** : installé via `brew install --cask android-commandlinetools`
|
||
→ `/opt/homebrew/share/android-commandlinetools` (524 MB)
|
||
- licences acceptées : `yes | sdkmanager --licenses`
|
||
- paquets : `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 3**, migrations explicites (⚠️ plus de
|
||
`fallbackToDestructiveMigration` — retiré en v1.2.0 car l'utilisatrice a des données
|
||
réelles ; toute évolution de schéma = `Migration(x, y)` + ALTER TABLE) :
|
||
|
||
- **v1 → v2** : `forecastIntervalDays REAL` (prévision, v1.2.0)
|
||
- **v2 → v3** : `calendarEventId INTEGER` (agenda récurrent, v1.3.0)
|
||
|
||
### `Treatment` (treatments)
|
||
- base : `id`, `name`, `type` (ESTRADIOL/ANTI_ANDROGEN/PROGESTOGEN/OTHER), `route`
|
||
(ORAL/TRANSDERMAL_GEL/TRANSDERMAL_PATCH/INJECTION_IM/INJECTION_SUBCUT/OTHER),
|
||
`doseAmount`, `doseUnit`, `isActive`, `notes`, `createdAt`
|
||
- PK par table : `esterType` ("NONE"/"EV"/"EU"/"EEN" — objets `Esters`), `pkModel`
|
||
("ESE"/"TFS" — objets `PKModels`)
|
||
- PK Bateman : `absorptionHours` (Tmax), `eliminationHalfLifeHours`, `bioavailabilityFraction`
|
||
- Calibration : `scaleFactor` (défaut 1.0)
|
||
- **Prévision (v1.2)** : `forecastIntervalDays: Double?` (jours ; null = pas de
|
||
simulation à venir) — colonne ajoutée par la **migration Room v1→v2**
|
||
- Rappel : `reminderHour/Minute/Enabled`
|
||
- Helpers : `isInjection` (IM/SC), `usesProfileModel` (injection **et** ester ≠ NONE)
|
||
|
||
### `DoseLog` (dose_logs)
|
||
FK → treatments (CASCADE), index `treatmentId` + `timestamp`. `esterType: String?` =
|
||
**override par injection** (l'ODS permet de switcher d'ester d'une injection à l'autre) ;
|
||
null = ester du traitement.
|
||
|
||
### `LabResult` (lab_results)
|
||
`marker` libre ("E2", "T", "PRL"…), `value`, `unit` libre. La calibration et les charts
|
||
comparent `marker.equals("E2", true)` / `"T"` — **les dropdown suggèrent E2/T** ; si
|
||
l'utilisatrice tape autre chose, la calibration ignorera ces labs.
|
||
|
||
### DAOs
|
||
`Flow` pour l'UI + one-shots `suspend *Once()` pour backup/boot/calibration :
|
||
`TreatmentDao.getActiveOnce/getAllOnce`, `DoseLogDao.getAllOnce`, `LabResultDao.getAllOnce`.
|
||
|
||
### 6.bis Sémantique `isActive` (v1.2.4) — drapeau administratif, JAMAIS un filtre
|
||
|
||
`Treatment.isActive = false` signifie « ce traitement ne produit plus de nouvelles
|
||
doses ». **Ce n'est PAS un filtre de données** — passer un traitement à inactif ne
|
||
doit rien effacer (bug v1.2.4 : Home/Chart collectaient `activeTreatments`, la
|
||
simulation de tout l'historique EV disparaissait) :
|
||
|
||
| Effet de l'inactivation | Où |
|
||
|---|---|
|
||
| ✅ Retiré des chips « Log rapide » (accueil) | `HomeScreen` → `activeTreatments` |
|
||
| ✅ Retiré du dropdown des **nouvelles** doses | `DosesScreen` : création → actifs only ; **édition → tous** (une dose existante reste rattachée à son traitement inactif) |
|
||
| ✅ Rappels annulés (au save, au boot, au calcul de « prochaine dose ») | `rescheduleAll` + `nextReminderFireMs` (filtre `isActive`) ; `BootReceiver` lit `getAllOnce()` pour annuler les alarmes des inactifs |
|
||
| ❌ JAMAIS retiré de la simulation | Home/Chart passent **`allTreatments`** au moteur — les écrans ne filtrent jamais |
|
||
| ❌ JAMAIS retiré de la calibration (auto) | `autoCalibrated(allTreatments, …)` |
|
||
| ❌ JAMAIS supprimé de l'historique des doses | `DosesScreen` liste tout, suppression manuelle uniquement |
|
||
|
||
Épinglé par la régression n°3 (`RegressionUserCase3Test`, cf §8) : EV inactif + EEn
|
||
actif en même temps, les 22 labs couvrant les deux périodes doivent rester simulés
|
||
et calibrés.
|
||
|
||
## 7. Moteur pharmacocinétique
|
||
|
||
`pk/PharmacokineticEngine.kt` + `pk/PKProfileStore.kt`.
|
||
|
||
### 7.1 Source : `Estrogen.ods`
|
||
|
||
- Fichier : le tableur `Estrogen.ods` de l'utilisatrice (Owncloud, 30 MB, 13 tables)
|
||
- 6 tables nominatives (surnoms anonymisés) : historique injections (datetime, cuisse
|
||
L/R, ester, dose mg, Z-track) + labs (E2 pg/mL, T ng/mL) + **facteur d'échelle**
|
||
manuel par profil (valeurs entre 0,5 et 1,4)
|
||
- Table **« Models »** : paramètres D, k1, k2, k3 par ester×modèle + **profils horaires
|
||
normalisés (pg/mL par mg) sur 8001 h** — ce sont ces tables qui sont consommées
|
||
- Les profils affichent 2 décimales → **plancher 0,01 / 0,00** en queue (conséquence
|
||
importante, cf §7.2)
|
||
|
||
Pics de référence (pg/mL par mg) :
|
||
|
||
| Clé | Modèle | Ester | Pic | Tmax |
|
||
|---|---|---|---|---|
|
||
| `EV_ese` | Estrannaise | valerate | 61,12 | ~45 h |
|
||
| `EU_ese` | Estrannaise | undecylate | 3,44 | ~55 h (plateau très long) |
|
||
| `EEn_ese` | Estrannaise | enanthate | 31,35 | ~152 h |
|
||
| `EV_tfs` | Transfem Science | valerate | 58,96 | ~51 h |
|
||
| `EU_tfs` | Transfem Science | undecylate | 10,11 | ~198 h |
|
||
| `EEn_tfs` | Transfem Science | enanthate | 31,97 | ~156 h |
|
||
|
||
### 7.2 `PKProfileStore` (asset loader + échantillonnage)
|
||
|
||
- Asset `app/src/main/assets/pk_profiles.json` : `{ "params": {D/k1/k2/k3…},
|
||
"profiles": { "EV_ese": [8001 floats], … } }` (550 KB, parse ~ms via `JsonParser`)
|
||
- **`initWithJson(json)`** = point d'entrée testable (JVM) ; `init(context)` lit l'asset
|
||
- `sample(ester, model, dtHours)` :
|
||
- modèle **strict** : seul "TFS"→`tfs` et "ESE"→`ese` ; tout autre → 0 (piège corrigé,
|
||
cf §14)
|
||
- interpolation **linéaire** entre heures entières
|
||
- **extrapolation terminale** : dernier point **≥ 1 % du pic** (pour éviter le plancher
|
||
d'affichage 0,01/0,00 de l'ODS), pente = décroissance moyenne sur les 48 h précédentes
|
||
(jamais avant le pic)
|
||
- ⚠️ tous les calculs en **Double** (Float×Double n'existe pas en Kotlin — source d'erreurs
|
||
de compilation, cf §14)
|
||
|
||
### 7.3 Superposition
|
||
|
||
Contribution d'une dose = `sample(...) × dose_mg` ; niveau total = somme des contributions
|
||
de toutes les doses E2, chacune multipliée par le `scaleFactor` de son traitement.
|
||
Coupure par dose : `cutoffHours` = longueur de table (8001 h) pour les profils,
|
||
`30 × t½` pour Bateman.
|
||
|
||
### 7.3b Override de modèle + prévision + auto-calibration (v1.2)
|
||
|
||
- **`modelOverride`** : paramètre optionnel de `concentrationOfDose` / `e2At` /
|
||
`computeCurve` qui force ESE ou TFS pour les traitements par profil — le graphique
|
||
dessine les deux modèles côte à côte depuis le même traitement (Bateman non concerné :
|
||
les deux séries y sont identiques).
|
||
- **`generateForecastDoses(treatment, doseLogs, toMs, nowMs)`** : projette les doses à
|
||
venir = dernière dose réelle + k × `forecastIntervalDays`, **strictement après
|
||
`nowMs`** (v1.2.7 : les créneaux déjà passés — cas d'un OUBLI — sont sautés au
|
||
rythme configuré, sinon faux pic dans l'historique ; un RETARD décale toute la
|
||
prévision) ; dose = standard du traitement, ester = override de la dernière
|
||
injection. Jamais persistées : uniquement passées à `computeCurve` par le
|
||
ChartScreen quand le chip « Prévision » est actif.
|
||
- **`autoCalibrated(treatments, doseLogs, labs, tConfig)`** : option « Calibration
|
||
automatique » (v1.2.1+) — renvoie `esterScales` (facteur **par ester**, cf §7.6)
|
||
+ `tKPerEster` (k T **par ester**, cf §7.5) recalculés depuis les labs ;
|
||
`treatments` et `tConfig` **inchangés** (fallback par ester absent = valeurs
|
||
stockées). HomeScreen et ChartScreen branchent dessus quand l'option est active.
|
||
|
||
### 7.4 Bateman (gel/patch/oral)
|
||
|
||
`C(dt) = (F·D·ka/(ka−ke))·(e^(−ke·dt) − e^(−ka·dt))` ; cas dégénéré ka≈ke :
|
||
`F·D·ke·dt·e^(−ke·dt)`. **`computeKa`** résout `ln(ka/ke) = (ka−ke)·Tmax` par bisection
|
||
(50 itérations, bornes `ke×1.001 … ke×1000`) — **direction corrigée** (cf §14 : `eq > 0`
|
||
⇒ la racine est **au-dessus** de mid ⇒ `lo = mid`).
|
||
|
||
### 7.5 Courbe T (empirique)
|
||
|
||
`T(t) = floor + (base − floor) / (1 + k(ester actif) · E2_calibrée(t))` [ng/mL].
|
||
Défauts `TConfig` : base 6.0, floor 0.2, k 0.19 (→ T≈0,4 à E2≈150). **Non issu du
|
||
`.ods`** (qui ne modélise pas la T) — modèle d'inhibition simple, étiqueté
|
||
« estimation » partout.
|
||
|
||
- **k par période d'ester** (v1.2.3) : la suppression T diffère selon l'ester
|
||
(valerate = pics hauts et courts, enanthate = plateau plus doux) →
|
||
`computeTKPerEster` attribue chaque lab T à la période de la dernière dose ≤ lab
|
||
et k = **médiane** des k de cette période ; la courbe utilise à chaque instant le
|
||
k de l'ester **actif** (`activeEsterAt`, curseur sur les doses triées dans
|
||
`computeCurve`), fallback = `tConfig.k` stocké.
|
||
- Formule : `k_i = ((base−floor)/(T_lab − floor) − 1)/E2_est(t_lab)`, garde
|
||
k ∈ (1e-4, 10). ⚠️ L'E2 utilisée est la version **calibrée** (scalePerEster) —
|
||
calibrer k contre une E2 brute faussait les k (corrigé v1.2.3).
|
||
- `computeTConfigCalibration` (k global unique) reste pour le bouton manuel
|
||
« Calibrer avec les analyses » des Paramètres.
|
||
|
||
**Unités** : les labs T peuvent être saisis en ng/mL, ng/dL, ng/L ou nmol/L —
|
||
`convertTToNgMl(value, unit)` normalise (ng/dL ÷100, ng/L ÷1000, nmol/L ×0,2884,
|
||
pg/mL ÷1000 défensif) ; appliqué à la calibration ET au rendu du chart (sinon l'axe
|
||
T est faux d'un facteur 100, bug réel remonté par l'utilisatrice : labs 33/44 ng/dL).
|
||
|
||
### 7.6 Calibration (v1.2.1 : PAR PÉRIODE D'ESTER pour l'auto)
|
||
|
||
**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(treatments, doseLogs, now) / nextReminderFireFor /
|
||
batemanParams / concentrationOfDose / computeKa / doseEster / isInjectionRoute`.
|
||
Type de retour : `LevelPoint(timestamp, e2, t)`.
|
||
|
||
|
||
### 7.8 Modèle TFS V3C (v1.4.0) — `pk/TransfemScienceModels.kt`
|
||
|
||
Le modèle « Transfem Science » n'est PLUS dérivé des tables ODS : c'est la
|
||
**forme close exacte** du modèle à 3 compartiments de la méta-analyse
|
||
(https://transfemscience.org/articles/injectable-e2-meta-analysis/), avec les
|
||
paramètres D/k1/k2/k3 publiés par le simulateur officiel
|
||
(transfemscience.github.io/injectable-e2-simulator — fichiers `ester-data.js`
|
||
/ `calc-curve.js`, valeurs = données scientifiques des études) :
|
||
|
||
```
|
||
Cp(t) = D·k1·k2 · [ e^(−k1·t)/((k1−k2)(k1−k3)) + e^(−k3·t)/((k1−k3)(k2−k3))
|
||
+ e^(−k2·t)·(k3−k1)/((k1−k2)(k1−k3)(k2−k3)) ] (t en JOURS)
|
||
```
|
||
|
||
- `sample(ester, dtHours)` = `Cp(t_jours) / fitDose(5 mg)` → pg/mL PAR mg ;
|
||
le moteur multiplie par la dose réelle, comme pour les tables ODS.
|
||
- Branchement dans `concentrationOfDose` : si `model == TFS` et que l'ester a
|
||
un V3C (EV, EEn, EU, EB, EC, ECS, PEP) → `TransfemScienceModels.sample` ;
|
||
sinon fallback tables ODS (Estrannaise, ou ester sans modèle).
|
||
- **Coupure moteur** (`cutoffHours`) : 10 demi-vies TERMINALES (t½ = ln2 /
|
||
min(k1,k2,k3)) — ex. EV 30 j, PEP 284 j — au lieu de la longueur de table.
|
||
- Compartiment k2 « ultra-rapide » (EU/PEP : k2 ~10⁵–10⁶ j⁻¹) : son terme
|
||
décroît instantanément, double précision suffisante (testé).
|
||
- Sanity checks épinglés (±1–3 %) : pics/t½ des Tableaux 9–10, AUC par mg
|
||
(EV 377,2 / EEn 436,6 / EC 430 / PEP 65,1 pg·j/mL/mg), état d'équilibre
|
||
EV 5 mg/7 j (Cmax 384 / Cmin 142 / Cavg 269, Figure 11 de l'article).
|
||
- ⚠️ EU (undécylate) : ajustement sur données d'étude LIMITÉES (cf article
|
||
§Limites) — précision moindre, conservé car c'est le meilleur disponible.
|
||
- Les valeurs des pics par mg restent cohérentes avec l'ancienne table ODS
|
||
(la table en DÉRIVAIT) : EV 59 pg/mL/mg @ 51 h, EEn 31,97 @ 156 h — le
|
||
changement est la FORME (queues exactes, pas d'arrondi 2 décimales).
|
||
|
||
## 8. Tests unitaires
|
||
|
||
**137 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — **115 sans
|
||
les données de test locales** (cf §8.bis : les 3 classes de régression,
|
||
6 tests chacune, sont skippées via `Assume`). Dépendance : JUnit 4.13.2.
|
||
Emplacement : `app/src/test/java/com/hormonetrack/`. Répertoire de travail d'exécution =
|
||
`app/` → l'asset est lu via `src/main/assets/pk_profiles.json` (fallback `app/src/…`).
|
||
|
||
- **`PKProfileStoreTest`** (8) : les 6 profils présents (8001 pts) ; pic EV_ese = 61,12
|
||
@45 h ; zéro avant injection ; interpolation stricte entre points (heures 100/101 —
|
||
le plateau 45/46 est plat, piège de test) ; modèle inconnu → 0 ; extrapolation
|
||
terminale décroissante ; esters longs mesurables à 8000 h ; **les 6 pics == valeurs ODS**
|
||
- **`PharmacokineticEngineTest`** (15) : ke = ln2/t½ ; **pic Bateman ≈ Tmax** (attrape la
|
||
bisection inversée) ; EV 4 mg → pic ≈ 4×61 pg/mL ; superposition ; linéarité du
|
||
scaleFactor ; anti-androgène → 0 en E2 ; modèle T monotone/borné ; calibration SF =
|
||
médiane (0,5/0,9/1,4 → 0,9) ; calibration nulle sans labs ; **calibration T récupère un
|
||
k planté (0,25)** ; grille horaire clampée à la 1ʳᵉ dose ; vide sans doses ; override
|
||
d'ester par dose (EV≫EU à 45 h) ; prochain rappel dans le futur ; levelAt combiné
|
||
- **`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.**
|
||
- **`RegressionUserCase3Test`** (6) : 3ᵉ régression (export **v1.3.1** — mis à
|
||
jour à la v1.3.2) — le scénario **transition** : traitement **EV inactif**
|
||
(29 doses 2–8 mg, janvier→juillet) + traitement **EEn actif** (9 doses) +
|
||
**CPA oral** (anti-androgène), soit 3 traitements, 51 doses, 26 labs.
|
||
Vérifie : parsing, **l'inactif reste simulé** (bug v1.2.4 — le moteur reçoit
|
||
TOUS les traitements), calibration par période couvrant EV **et** EEN,
|
||
k T par ester, continuité de la courbe pendant la transition, niveau
|
||
actuel ~EEn équilibre.
|
||
|
||
### 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`, `backup-v1.2.0.json`,
|
||
`backup-v1.2.3.json` (ancien export, plus consommé par un test),
|
||
`backup-v1.3.1.json` et `backup-v1.4.2.json` (copiés tels quels depuis
|
||
l'export JSON de l'app) ; `RegressionUserCaseTest` lit v1.0.0,
|
||
`RegressionUserCase2Test` lit v1.2.0, `RegressionUserCase3Test` lit
|
||
**v1.3.1**, `RegressionUserCase4Test` lit **v1.4.2** (premier export avec
|
||
les settings v2) ;
|
||
- `.gitignore` contient `local-test-data/` → jamais commités.
|
||
**GARDES DE CONFIDENTIALITÉ (rappel 7 sept. 2026, à chaque release)** :
|
||
1. `git check-ignore -v local-test-data/…` → la règle matche ;
|
||
2. `git ls-tree -r <tag> --name-only | grep local-test-data` → VIDE avant
|
||
publication ;
|
||
3. `git log --all -- local-test-data/` → vide (aucun commit n'y a jamais
|
||
touché — vérifié à la v1.4.3) ;
|
||
4. le scan de confidentialité (working tree + §8.bis dette historique) ;
|
||
5. les assertions des régressions sont DATA-DRIVEN (elles lisent le
|
||
fichier local) — JAMAIS de valeur de lab ni de timestamp réel en dur
|
||
dans le code (le scanner refuserait, et ce serait une fuite).
|
||
- les tests font `Assume.assumeTrue(file.exists())` dans le `@Before` : **sans le
|
||
fichier, la classe est IGNORÉE** (skipped, pas failed) — un clone neuf ou une CI
|
||
exécute 44 tests au lieu de 87 ;
|
||
- le workdir des tests Gradle est le dossier du module (`app/`) → les tests
|
||
cherchent les fichiers à plusieurs chemins (`../local-test-data/…` en premier) ;
|
||
- **pour les lancer** : exporter un backup JSON depuis l'app → l'enregistrer sous
|
||
le nom attendu dans `local-test-data/` → `./gradlew testDebugUnitTest` ;
|
||
- ⚠️ **ne jamais embarquer ces données dans les tests** (string inline dans le
|
||
code) : tout nouvel export réel → fichier gitignore + assertions data-driven ;
|
||
- **l'historique a été nettoyé avant le premier push** : les premiers commits
|
||
embarquaient les valeurs (tests + doc) → `git filter-branch --tree-filter` avec
|
||
un script d'anonymisation (timestamps décalés de +30 j, valeurs perturbées dans
|
||
la prose des docs), tags réécrits, refs purgeées.
|
||
⚠️ **Lint de dette connue (constat v1.3.3)** : le scanner travaillait sur une liste
|
||
de motifs figée ; depuis que `local-test-data/` s'enrichit de NOUVEAUX exports
|
||
(ex. backup-v1.3.1.json ajouté à la v1.3.2), la ré-extraction des valeurs fait
|
||
que le scan `--history` détecte des fragments JSON de labs **encore présents
|
||
dans l'historique des révisions v1.1.0 → v1.2.3** (tests `RegressionUserCase{,2}`
|
||
data-driven AVANT la migration fichiers gitignorés ; fragments de la forme
|
||
« value » + timestamp). Amplitude : quelques valeurs numériques de labs — les
|
||
mêmes ordres de grandeur figurent déjà en PROSE dans les docs publiés (§14 #23,
|
||
CHANGELOG 1.1.0). Le working tree est propre. **Décision utilisateur (6 sept.
|
||
2026)** : purge complète de l'historique NON relancée (les révisions sont déjà
|
||
répliquées sur les deux Gitea, aucune fuite nouvelle par push) ; la dette
|
||
(réécriture d'historique + re-tags + force-push des 2 remotes + re-publish des
|
||
releases) reste ouverte et doit être re-décisionnée si le dépôt passe PUBLIC.
|
||
- **`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).
|
||
- **`ChangelogHelperTest`** (8) : v1.3.0 — comparaison SemVer **numérique**
|
||
(1.2.9 < 1.2.10 : la comparaison lexicographique serait fausse), extraction
|
||
de sections depuis la dernière vue, première installation (section courante
|
||
seule), plusieurs versions intermédiaires, versions non numériques.
|
||
- **`AppLogTest`** (4) : v1.3.1 — formatage des lignes horodatées, buffer
|
||
circulaire (600 → 500 dernières, bordures).
|
||
- **`HrtDurationTest`** (6) : v1.3.1 — jours depuis la 1re prise (0 si
|
||
futur/invalide), décomposition mois(30 j)/jours.
|
||
- **`CalendarRruleTest`** (3) : v1.3.0 — RRULE `FREQ=DAILY;INTERVAL=N`,
|
||
arrondi demi-supérieur des décimales (6,5 → 7), clamp ≥ 1 jour.
|
||
- **`ExtremaTest`** (6) : v1.2.3 — détection des pics/creux (`detectExtrema`) :
|
||
alternance stricte pic/creux en régime d'équilibre (4 doses hebdo → ≥ 4 extrema,
|
||
pic > creux voisin, valeurs dans les bornes), courbe monotone → vide, série plate →
|
||
vide, série trop courte → vide, **filtre d'amplitude** (sémantique zigzag : une
|
||
oscillation sous le seuil produit UN pivot, l'alternance complète apparaît quand le
|
||
seuil baisse), anti-corrélation E2/T (un pic d'E2 ≈ un creux de T).
|
||
- **`TransfemScienceModelsTest`** (5) : v1.4.0 — épingle le modèle TFS V3C sur
|
||
l'article : pics/t½ des Tableaux 9–10 (±1–2 %), AUC par mg (±3 %), état
|
||
d'équilibre EV 5 mg/7 j = Figure 11 (Cmax 384/Cmin 142/Cavg 269), esters
|
||
sans modèle → 0, dt ≤ 0 → 0. **Toute retouche de TransfemScienceModels.kt
|
||
passe par CE test** (c'est la fidélité au simulateur TFS qui est vérifiée).
|
||
- **`RegressionUserCase4Test`** (4) : v1.4.3 — 4ᵉ régression épinglée sur
|
||
les données réelles (export v1.4.2, HORS dépôt) : parsing v2 avec
|
||
**settings** (seuils + auto-cal), scénario transition EV inactif → EEn
|
||
actif, **signature pharmacocinétique EEn** (contribution d'une dose à
|
||
J+1 < 12 % du total, plateau 48 h ≤ 25 % — c'est la réponse épinglée à
|
||
« pourquoi l'estimation ne remonte pas le lendemain »), pas de fausse
|
||
alerte à l'observation. **Tout nouvel export = une régression data-driven.**
|
||
- **`AlertsTest`** (12) : v1.4.2 — logique pure des seuils (`pk/Alerts.kt`) :
|
||
HIGH/LOW, comparaison STRICTE (valeur == limite → rien, éviter le
|
||
clignotement d'arrondi), seuil null → jamais d'alerte, cohérence haut > bas,
|
||
HIGH prime LOW en config incohérente (défensif), les deux marqueurs à la
|
||
fois (E2 puis T), **codec d'état** (`encodeState`/`parseState` — rétro-résistant
|
||
aux entrées malformées) et **décision de notification** (`shouldNotify` :
|
||
nouveau franchissement → notif, même état → pas de spam, retour à la
|
||
normale → pas de notif mais ré-arme).
|
||
- **`AlertsEngineTest`** (4) : v1.4.2 — câblage moteur → seuils : le niveau
|
||
évalué par la NOTIFICATION (`levelAt(scalePerEster)`) est celui de la
|
||
CARTE accueil (`computeCurve` dernier point, ±10 % de dérive horaire) ;
|
||
seuil à la moitié du niveau réel → HIGH, seuil au double → rien ; T bas
|
||
→ LOW en ng/mL ; l'état persisté ne contient que les marqueurs en alerte.
|
||
- **`TransfemScienceEngineTest`** (4) : v1.4.0 — câblage MOTEUR ↔ V3C :
|
||
EB (sans table ODS) routé en TFS → pic article 971 pg/mL @ 0,65 j ;
|
||
EB forcé en ESE (`modelOverride`) → 0 partout (comportement documenté :
|
||
presets EB/EC/ECS/PEP en TFS uniquement) ; PEP 32,5 mg → contribution
|
||
à 10 j ET 100 j (l'ancienne coupure 24 h du fallback table aurait donné 0)
|
||
et 0 au-delà de 10 × t½ (par design) ; `computeCurve(modelOverride=TFS)`
|
||
sur un traitement stocké ESE reproduit le pic V3C EV 295 — c'est le test
|
||
du dispatch `concentrationOfDose` + de `cutoffHours`.
|
||
- **`ChartZoomTest`** (7) : v1.2.9 (pas d'échantillonnage) + v1.4.1 — **horizon de prévision** 12 × Posologie borné 30 j–1 an (`forecastHorizonHours`, null sans Posologie) et **clamp du panoramique** (`clampPanHours` : panHours négatif = futur, borné par l'horizon ; prévision OFF → futur interdit) — la logique de fenêtre du graphique est centralisée et testée.
|
||
- **`ReminderScheduleTest`** (8) : v1.4.0 — fix #52 : rappel sur la GRILLE
|
||
Posologie (dose il y a 2 j + intervalle 7 j → alarme dans 5 j à l'heure
|
||
choisie, PAS demain), heure déjà passée le jour du créneau → créneau
|
||
suivant, créneau manqué sauté (jamais dans le passé), fallback quotidien
|
||
sans Posologie ou sans doses, agrégat multi-traitements (le plus tôt
|
||
gagne), traitements inactifs/désactivés → null.
|
||
|
||
**Ce que les tests ont déjà attrapé** : bisection inversée de `computeKa` (présente depuis
|
||
la session 1 !), plancher 0,01 des queues de profils, mapping silencieux du modèle inconnu.
|
||
**Toute modification du moteur passe par ces tests.** Suite envisageable : Robolectric
|
||
(UI/logic Android), tests Compose, lint.
|
||
|
||
## 9. Système de rappels
|
||
|
||
`reminder/ReminderManager.kt` (+ `DoseActionReceiver.kt`, `ReminderReceiver`).
|
||
|
||
- `ReminderContract` : constantes + **fabrique unique `reminderIntent()`** pour schedule
|
||
ET cancel (même action = même PendingIntent — cf bug §14.3)
|
||
- `AlarmScheduler` :
|
||
- **`scheduleFor(treatment, doseLogs)` (v1.4.0, source de vérité = moteur)** :
|
||
la date du prochain déclenchement vient de
|
||
`PharmacokineticEngine.nextReminderFireFor` — **grille Posologie** si le
|
||
traitement a `forecastIntervalDays` + des doses (rappel UNIQUEMENT le jour
|
||
du créneau, à l'heure choisie — fix #52), sinon quotidien (comportement
|
||
historique, gel/oral ou app sans historique)
|
||
- alarmes **one-shot** (`setExactAndAllowWhileIdle`) : la suivante est
|
||
reprogrammée par `ReminderReceiver` (après notif) et `DoseActionReceiver`
|
||
(après « Pris ») — avant v1.4.0 la chaîne ne se recousait qu'au boot/au save
|
||
- permission **SCHEDULE_EXACT_ALARM** : bouton d'octroi dans Paramètres + éditeur
|
||
(`Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM`)
|
||
- `scheduleSnooze` (+1 h), `rescheduleAll(treatments, doseLogs)`
|
||
- `ReminderReceiver` : notif HIGH/REMINDER, 2 actions + tap → MainActivity
|
||
(`open_log_dose`, `treatment_id`) → Home ouvre le dialog pré-rempli ; requestCodes
|
||
PendingIntent = `id*10+{0,1,2}` ; notificationId = `id.toInt()` ; **puis
|
||
reprogramme le prochain déclenchement** (goAsync + IO)
|
||
- `DoseActionReceiver` (non exporté) : **« Pris »** → `goAsync()` + coroutine IO → insert
|
||
DoseLog (dose = extra ou standard) **+ reprogramme** (la dose loguée avance
|
||
la grille) ; **« Reporter 1 h »** → `scheduleSnooze` ; annule la notif
|
||
- `BootReceiver` : `goAsync()` + thread + **`runBlocking`** + one-shots
|
||
`getAllOnce()` (traitements) + `doseLogDao.getAllOnce()` → `rescheduleAll`
|
||
(jamais un Flow en runBlocking !)
|
||
|
||
**Événements d'agenda (v1.3.0, `reminder/CalendarEvents.kt`)** : switch dans
|
||
l'éditeur (sous Rappels) → insertion dans un **calendrier local dédié
|
||
« HormoneTrack »** (CalendarContract, ACCOUNT_TYPE_LOCAL) d'un événement
|
||
récurrent RRULE `FREQ=DAILY;INTERVAL=N` (N = Posologie, arrondi demi-supérieur
|
||
**explicite** : `floor(x + 0.5)` — kotlin.math.round arrondit les ties vers
|
||
l'entier PAIR !), début = prochaine occurrence à l'heure de rappel (ou 12:00).
|
||
L'id est stocké sur le traitement (`calendarEventId`, Room v3) ; au save :
|
||
supprimer + recréer (fiable) si changé, supprimer si désactivé. Permissions
|
||
WRITE_CALENDAR + READ_CALENDAR demandées à l'activation du switch.
|
||
Manifest : `POST_NOTIFICATIONS`, `SCHEDULE_EXACT_ALARM`, `RECEIVE_BOOT_COMPLETED`,
|
||
`VIBRATE`, `WRITE_CALENDAR`, `READ_CALENDAR`.
|
||
⚠️ **Permissions agenda (v1.3.3)** : WRITE_CALENDAR + READ_CALENDAR ont été
|
||
ajoutées au manifest seulement en v1.3.3 — elles étaient documentées ici
|
||
depuis v1.3.0 mais **jamais déclarées dans le XML** (bug #44, §14) : tout
|
||
jeton « le manifest contient X » doit être VÉRIFIÉ dans le fichier réel.
|
||
Sur la montre : remontée par Gadgetbridge **ou** Huawei Health (cf §17).
|
||
|
||
### 9.bis Alertes de seuil : `reminder/AlertNotifier.kt` + worker (v1.4.2)
|
||
|
||
- **Seuils** (`pk/Alerts.kt`, PUR) : `Thresholds` (E2 haut/bas pg/mL, T haut/bas
|
||
ng/mL, null = pas de limite), évaluation STRICTE (valeur == limite → rien),
|
||
`evaluateAll` (E2 puis T), validation haut > bas (`isCoherent`).
|
||
- **Worker WorkManager** : `AlertWorker` (CoroutineWorker) périodique **15 min**
|
||
(minimum WorkManager) + **one-time au save des seuils** (feedback immédiat) ;
|
||
planifié dans `HormoneTrackApp.onCreate` (`ExistingPeriodicWorkPolicy.KEEP`,
|
||
survit aux reboots — BootReceiver n'a RIEN à faire).
|
||
- **Calcul du niveau** : `levelAt(..., scalePerEster, tKPerEster)` — MÊME
|
||
calibration que la carte accueil (auto-cal incluse si l'option est active).
|
||
- **Anti-spam** : l'état des alertes déjà notifiées est persisté en DataStore
|
||
(`alert_notified_state` = `Alerts.encodeState`, ex. « E2:HIGH;T:LOW ») ;
|
||
`shouldNotify` : nouveau franchissement ou changement H↔L → notif, même
|
||
état → rien, retour à la normale → état effacé (ré-arme), pas de notif de retour.
|
||
- **Canal dédié** `hormonetrack_alerts` (IMPORTANCE_HIGH) : réglable
|
||
indépendamment des rappels ; notification avec texte multi-lignes (BigText),
|
||
tap → MainActivity. Garde défensive POST_NOTIFICATIONS (API 33+) journalisée.
|
||
- Journalisation AppLog complète (« notification : E2:HIGH... », « franchissement
|
||
inchangé », « retour à la normale », erreurs worker) — diagnostic à distance.
|
||
|
||
## 10. UI & navigation
|
||
|
||
- `HormoneTrackRoot` : NavigationBar 5 tabs (home/chart/doses/labs/treatments) + routes
|
||
`settings`, `treatment_edit/{id}` (-1 = nouveau) ; barre masquée sur ces 2 routes
|
||
- `HomeScreen` : bandeau gradient (TransSky→TransPink, discret), carte **niveau actuel**
|
||
(E2 ≈ X pg/mL, T ≈ Y ng/mL, delta vs 6 h — **v1.4.3 : comparaison au point le
|
||
plus proche de −6 h via `pointHoursBefore`, l'ancien code comparait ~24 h**,
|
||
bug #53), **cartes d'alerte v1.4.2** (si un
|
||
seuil est franchi : fond `errorContainer`, « ▲ E2 estimé ≈ X pg/mL — au-dessus
|
||
de ta limite (Y) », basées sur le DERNIER point de courbe = niveau affiché,
|
||
disclaimer « estimation, pas une mesure »), carte prochaine dose (**v1.4.1 :
|
||
delta en JOURS au-delà de 24 h — « 5 j 2 h · sam. 6 18:00 » via
|
||
`HrtDuration.daysAndHours` + `next_dose_days` ; en dessous, h/min**), chips de log rapide
|
||
(+ FAB), mini-chart 24 h (multi-séries via `ChartSeries`) **cliquable → écran
|
||
Graphiques** (v1.2.1) avec mini-légende E2/T ; données auto-calibrées si l'option est
|
||
active (`scalePerEster = effectiveAuto?.esterScales`) ; rafraîchissement `tick` 60 s
|
||
- `ChartScreen` (v1.2, le plus riche) : **fenêtre temporelle v1.4.1** —
|
||
`endMs = now − panHours` avec `panHours` signé : > 0 = passé (tirer vers la
|
||
droite), **< 0 = futur** (tirer vers la gauche, uniquement avec la
|
||
prévision active, borné par `forecastHorizonHours` = 12 × Posologie borné
|
||
30 j–1 an). **Activer le chip Prévision ne déplace PAS la fenêtre** (la
|
||
projection s'étend au-delà et se parcourt au drag) ; désactiver pendant un
|
||
voyage futur → retour à maintenant ; clamp centralisé dans
|
||
`clampPanHours` (testé). Plages 24 h/7 j/30 j + **boutons zoom − / +**
|
||
(v1.2.9) ; **panoramique**
|
||
(`detectHorizontalDragGestures` — tirer vers la droite remonte dans le passé,
|
||
`panHours` borné à [0, âge de la 1ʳᵉ dose + plage], bouton « Revenir à maintenant ») ;
|
||
**toggles indépendants Estrannaise/TFS** → deux `computeCurve` avec `modelOverride`
|
||
superposées (E2 ESE bleu plein, E2 TFS turquoise, T ESE rose plein, T TFS rose
|
||
pointillé) ; **chip Prévision** (doses projetées via `generateForecastDoses`, horizon
|
||
= 2× le plus grand intervalle configuré, borné 7–30 j) ; **auto-calibration** branchée
|
||
sur les Paramètres ; légende dynamique ; labs T normalisés en ng/mL ; **toggle T =
|
||
masque aussi les labs T** (v1.2.2) ; **chip « Pics / creux »** (v1.2.3 : triangles ▲▼
|
||
aux extrema locaux de chaque courbe, via `detectExtrema` — E2 seuil 2 pg/mL, T seuil
|
||
0,02 ng/mL)
|
||
- `DosesScreen` : LazyColumn par jour (desc), **Δ jours depuis la dose précédente du même
|
||
traitement** (`intervalsByDoseId`, colonne « Interval (d) » du `.ods`), suppression
|
||
avec confirmation, FAB → `DoseDialog` (création), **tap sur la ligne → édition** ;
|
||
**en-tête « temps sous THS »** (v1.3.1) : carte depuis la 1re prise, jours
|
||
totaux + décomposition mois/jours (`HrtDuration`, mois = 30 j estimés)
|
||
- `LabsScreen` (v1.2.2) : **prise de sang E2 + T en une entrée** (LabDialog create :
|
||
deux sections optionnelles → 1 ou 2 LabResult au même timestamp) ; affichage
|
||
**groupé par timestamp** (`groupLabsForDisplay` : E2 avant T, autres ensuite, tri
|
||
desc) → « E2 306 pg/mL · T 44 ng/dL » côte à côte ; tap → sélecteur E2/T si paire,
|
||
édition unitaire pré-remplie ; suppression = **la prise entière** (confirm nommant
|
||
les valeurs — choix de design : les labs sont prélevés ensemble) ; suggestions
|
||
d'unités pg/mL, ng/mL, ng/dL, ng/L, nmol/L, mIU/L
|
||
- `TreatmentsScreen` : cartes (nom, route, dose, chips ester·modèle / Tmax / ×scale / ⏰,
|
||
badge inactif), FAB → éditeur
|
||
- `TreatmentEditorScreen` : 12 presets (`PKPresets`, cf `nameRes`) pré-remplissent tout ;
|
||
champs conditionnels (ester+modèle si injection, Bateman sinon) ; carte Calibration
|
||
(scaleFactor + « Calibrer avec les analyses ») ; **section « Posologie » (v1.2 :
|
||
switch « Simuler les doses à venir » + intervalle en jours)** ; carte Rappel (switch +
|
||
TimePicker **centré** + avertissement alarmes exactes) ; switch actif ; save →
|
||
insert/update + schedule/cancel ; delete avec confirmation ; `createdAt` préservé
|
||
- **Logs de diagnostic (v1.3.1, `util/AppLog.kt`)** : journal horodaté
|
||
(buffer circulaire 500 lignes, persisté `filesDir/debug-log.txt`),
|
||
alimenté par l'agenda (permissions, upsert/delete), l'import/export et les
|
||
erreurs ; **export (SAF) et effacement** dans Paramètres — permet le debug
|
||
à distance (l'utilisateur joint les logs à son retour).
|
||
- **Dialog « Nouveautés » (v1.3.0)** : au démarrage, si `BuildConfig.VERSION_NAME`
|
||
est plus récente que la dernière vue (DataStore `changelog_seen_version`),
|
||
un AlertDialog affiche les sections CHANGELOG concernées (extraites par
|
||
`ChangelogHelper.sectionsSince`, asset `changelog.md` synchronisé par la
|
||
tâche `copyChangelog` — gitignoré, source de vérité = docs/CHANGELOG.md) ;
|
||
fermable, ne réapparaît pas avant la prochaine mise à jour.
|
||
- `SettingsScreen` : langue (Système/Français/English, chips reflétant l'état) ;
|
||
**carte « Seuils d'alerte » (v1.4.2)** : 4 champs optionnels (E2 haut/bas pg/mL,
|
||
T haut/bas ng/mL, vide = pas d'alerte), validation haut > bas (refus + message
|
||
rouge), Save → persiste + **check one-time immédiat** (`AlertNotifier.checkNow`) ;
|
||
**version installée + lien cliquable vers les releases Gitea** (v1.3.0,
|
||
`BuildConfig.VERSION_NAME`, `enableEdgeToEdge`-friendly) ;
|
||
**« Calibration automatique (E2 + T) » — UNE option** (par période d'ester + modèle T,
|
||
v1.2.1, désactivée par défaut) puis réglages T manuels (base/floor/k + bouton
|
||
« Calibrer avec les analyses » ponctuel) ; statut alarmes exactes + bouton d'octroi ;
|
||
Export/Import JSON ; à propos
|
||
- Composants : `CurveChart` (§11), `DateTimeField` (DatePicker+TimePicker Material3,
|
||
LocalDateTime, **horloge centrée**), `DoseDialog` (create/edit + override d'ester),
|
||
`LabDialog`, `formatDose()` (top-level, dans `DoseDialog.kt`)
|
||
- **Insets edge-to-edge (v1.2.5)** : `enableEdgeToEdge()` dans MainActivity ;
|
||
les insets sont consommés UNE FOIS — TopAppBar M3 (barre de statut, insets
|
||
par défaut) et NavigationBar (barre système) ; les `Scaffold` (racine
|
||
HormoneTrackRoot + imbriqués Doses/Labs) ont `contentWindowInsets = 0` pour
|
||
ne pas cumuler. Avant : fenêtre AppCompat + padding interne TopAppBar =
|
||
double espace vide en haut.
|
||
- Thème M3 custom (`ui/theme/Color.kt` : bleu #4F5BD5, rose #D6589E, labs orange, bandeau
|
||
TransSky/TransPink), dynamic color désactivé ; `MaterialExpressiveTheme` encore
|
||
**internal** dans la ligne material3 pinnée (cf §3) → `MaterialTheme` standard
|
||
- ⚠️ `Card(onClick=…)` et `ExposedDropdownMenuBox` = **API expérimentales M3** → `@OptIn`
|
||
requis sur chaque composable qui les utilise
|
||
|
||
## 11. Graphiques (CurveChart)
|
||
|
||
Canvas pur (aucune lib), **multi-séries** (v1.2) : `ChartSeries(points, e2Style, tStyle?)`
|
||
— le ChartScreen superpose les courbes Estrannaise **et** Transfem Science depuis le
|
||
même traitement (`modelOverride`), styles plein/pointillé par série. Dual axe : E2
|
||
gauche (pg/mL), T droite (ng/mL). Échelle « nice » (`niceCeil` : 1/2/2.5/5/10 × 10ⁿ)
|
||
partagée entre toutes les séries. Grille 4 lignes ; labels Y gauche/droite ; X : pas
|
||
6 h/24 h/5 j selon plage (`SimpleDateFormat` HH'h' / dd/MM). Labs : cercles (E2) et
|
||
carrés (T) orange + valeur, **T convertie en ng/mL** (`convertTToNgMl`) au rendu.
|
||
Ligne verticale « maintenant ». **Pics/creux** (v1.2.3, valeurs v1.2.6) :
|
||
triangles ▲▼ via `PharmacokineticEngine.detectExtrema` **accompagnés de la
|
||
valeur estimée** du pic (au-dessus) ou du creux (en dessous), dans la couleur
|
||
de la série (`drawExtremum`).
|
||
|
||
**Zoom (v1.2.9)** : pinch **et** boutons − / + — plage temporelle bornée
|
||
**6 h → 300 j** (`MIN_RANGE_H`/`MAX_RANGE_H`), point focal du pinch maintenu
|
||
fixe dans le temps (formule centroid), échantillonnage **adaptatif**
|
||
(`stepForRange` : 15 min ≤ 12 h, 30 min ≤ 24 h, sinon 1 h — courbes lisses à
|
||
fort zoom) et labels X adaptatifs (1 h / 3 h ajoutés). Le pan ET le zoom
|
||
partagent un seul `detectTransformGestures` (un handler = pas de conflit de
|
||
consommation) ; le zoom/pan restent gérés **par le parent**, le Canvas demeure
|
||
purement déclaratif. **Convention des boutons** (v1.2.10) : « + » = zoom avant
|
||
(fenêtre courte), « − » = zoom arrière — le sens initial était inversé (retour
|
||
utilisateur).
|
||
|
||
Pièges :
|
||
- `DrawScope` implémente `Density` → `X.dp.toPx()` direct ; ne PAS écrire de helper custom
|
||
- Tout label passe par `drawContext.canvas.nativeCanvas` + `android.graphics.Paint`
|
||
- Mélange Double/Float interdit (`1 - i / 4f` et pas `/4.0`)
|
||
- Le panoramique est géré **par le parent** (ChartScreen change `startMs/endMs`), pas par
|
||
le Canvas — le chart reste un composant purement déclaratif
|
||
|
||
## 12. i18n FR/EN
|
||
|
||
- Standard Android : `values/strings.xml` (EN défaut) + `values-fr/strings.xml` (FR).
|
||
L'objet `Strings.kt` custom de la session 1 a été **supprimé**.
|
||
- **Langue par app** : AppCompat 1.7 + `AppCompatDelegate.setApplicationLocales`
|
||
(fonctionne < API 33) ; choix persisté DataStore (`system`/`fr`/`en`), appliqué au
|
||
démarrage. Thème app = `Theme.AppCompat.DayNight.NoActionBar` (requis par AppCompat).
|
||
- Notifs localisées via `context.getString(R.string.*)`
|
||
- ⚠️ **Toute nouvelle string = les DEUX fichiers** (une référence manquante = erreur de
|
||
compilation `Unresolved reference 'active'` — déjà arrivé)
|
||
|
||
## 13. Sauvegarde JSON
|
||
|
||
`data/backup/BackupManager.kt` :
|
||
- `BackupData{version=2, exportedAt, treatments[], doseLogs[], labResults[], tConfig,
|
||
settings?}` → Gson — **v2 (v1.4.2)** : champ optionnel `settings: UserSettings`
|
||
(champs PLATS : language, autoCalibrate, alertE2High/Low pg/mL, alertTHigh/Low
|
||
ng/mL). **Rétrocompat** : les backups v1 (sans settings) restent parsables
|
||
(null) et importables ; l'import ne vérifie pas strictement la version.
|
||
`changelog_seen_version` volontairement EXCLU (pas une donnée utile à
|
||
restaurer). ⚠️ R8 : UserSettings est lue par réflexion Gson → `-keep`
|
||
explicite dans proguard-rules.pro (sinon objets vides en release seule).
|
||
- **Les IDs Room sont conservés** dans l'export et réinsérés tels quels → les FK
|
||
dose→traitement restent valides
|
||
- Import : **mode ÉCRASEMENT (v1.2.6)** — les données actuelles sont effacées
|
||
AVANT l'insertion (`deleteAllDoseLogs` → `deleteAllLabResults` →
|
||
`deleteAllTreatments`, ordre enfants → parents pour la FK CASCADE), puis
|
||
restauration avec les IDs du backup conservés (FK valides). Avant (v1.2.5) :
|
||
insertion en « ajout » → conflit d'ID dès que l'app contenait des données →
|
||
l'import échouait (bug remonté). Après un import : le `tConfig` du backup est
|
||
restauré dans DataStore, **les paramètres utilisateur sont restaurés**
|
||
(v1.4.2 : auto-calibration, seuils d'alerte, langue — la LANGUE est
|
||
appliquée sur le thread MAIN car `setApplicationLocales` recrée
|
||
l'activité) et les rappels sont reprogrammés (`rescheduleAll`). Le dialog prévient que TOUT sera
|
||
remplacé (bouton « Effacer & restaurer »)
|
||
- Transport : SAF (`CreateDocument("application/json")` / `OpenDocument`), écriture
|
||
`openOutputStream(uri, "wt")` ; ⚠️ pas de `return` dans un expression body `= try{}`
|
||
|
||
## 14. Bugs corrigés
|
||
|
||
Historique complet — **à ne pas réintroduire** (utile pour diff/revert) :
|
||
|
||
**Session 1 → 2 (avant tout build) :**
|
||
1. `settings.gradle.kts` : `dependencyResolution` (inexistant) → `dependencyResolutionManagement`
|
||
2. `BootReceiver` : `runBlocking { flow.collect {…} }` → blocage infini → one-shot + goAsync
|
||
3. `AlarmScheduler.cancel` : Intent sans l'action → annulation inopérante → fabrique unique
|
||
4. `PKProfileStore` : parsait la racine JSON → crash → lecture de `profiles`
|
||
|
||
**Session build (détectés à la compilation) :**
|
||
5. `kotlin.math.ln2` **n'existe pas** (hallucination) → `ln(2.0)` ; cascade d'erreurs sur
|
||
les lignes suivantes du même fichier (opérateurs sur types error)
|
||
6. Mélange **Double/Float** interdit en Kotlin : `mg * bioavailabilityFraction` (Float),
|
||
`30.0 * t½` (Float), `Float×exp()`… → `.toDouble()` partout
|
||
7. `BackupManager.writeBackup` : `return` dans expression body `= try{}` → block body
|
||
8. `DateTimeField` : `spacedBy(8f/2f*8)` (Float sans unité) → `8.dp` + import `dp` manquant
|
||
9. `DateTimeField` : extension `fun LocalDate.Companion.ofEpochMs` (java.time n'a pas de
|
||
Companion) → supprimée ; imports nettoyés
|
||
10. `CurveChart` : helper `dpToPx()` custom cassé → `dp.toPx()` de `DrawScope`
|
||
11. `CurveChart` : labels Y en Double (`i / 4.0`) → `i / 4f`
|
||
12. `LabsScreen` / `TreatmentsScreen` : imports `dp` / `fillMaxWidth` manquants
|
||
13. `TreatmentEditorScreen` : `R.string.active` inexistante → string ajoutée EN+FR
|
||
14. `TreatmentCard` : `Card(onClick=…)` sans `@OptIn(ExperimentalMaterial3Api::class)`
|
||
15. Typo `Locale.getDefault` sans parenthèses (SimpleDateFormat)
|
||
16. `SettingsScreen` : chips de langue codées en dur → état depuis DataStore
|
||
17. `ReminderManager` : constantes d'action mortes → implémentées (`DoseActionReceiver`)
|
||
18. `TreatmentEditorScreen` : `createdAt` écrasé à l'édition → préservé
|
||
|
||
**Session tests (bugs SÉMANTIQUES trouvés par les tests unitaires) :**
|
||
19. **`computeKa` : bisection inversée** — `if (eq > 0) hi = mid else lo = mid` convergeait
|
||
vers ka énorme (pic à ~0 h au lieu de Tmax) ; bug présent depuis la session 1, jamais
|
||
testé. → `if (eq > 0) lo = mid else hi = mid` (eq décroît en mid ; eq>0 ⇒ racine au-dessus)
|
||
20. **Plancher d'affichage des profils** : l'ODS arrondit à 2 décimales → queues à 0,01/0,00
|
||
; extrapoler depuis la fin de table donnait 0 à vie (ou une constante plate). →
|
||
extrapolation depuis le dernier point ≥ 1 % du pic avec pente sur 48 h
|
||
21. **Mapping silencieux du modèle** : `profileKey` mappe tout modèle ≠ "TFS" sur "ese"
|
||
→ `sample("EV","XXX")` renvoyait EV_ese. → validation stricte dans `sample`
|
||
|
||
**Session v1.1.0 (remontées par l'utilisatrice, reproduites en test) :**
|
||
22. **Casse des clés de profils** — LE bug « les graphiques ne se génèrent pas » :
|
||
l'asset contient `"EEn_ese"`/`"EEn_tfs"` (casing biologique du `.ods`) mais
|
||
`Esters.EEN = "EEN"` → lookup exact null → `sample()=0` pour tout traitement EEn
|
||
(courbe E2 plate à 0, T plate à la base). EV/EU marchaient (casse identique) et les
|
||
tests profils utilisaient la casse "EEn" — le trou passait entre les deux.
|
||
→ **lookup insensible à la casse** (`PKProfileStore.lookup()`), régression épinglée
|
||
sur les données réelles (`RegressionUserCaseTest`).
|
||
23. **Unités T non converties** : labs saisis en ng/dL (32/45) → axe T du chart à
|
||
×100 (courbe T invisible) et calibration T fausse. →
|
||
`PharmacokineticEngine.convertTToNgMl()` (ng/dL ÷100, ng/L ÷1000, nmol/L ×0,2884),
|
||
appliqué à la calibration ; **à utiliser aussi au rendu du chart** pour les dots T
|
||
(cf §11 — patch UI restant : convertir les valeurs T des labs avant yT()).
|
||
24. **Pas d'édition des doses** : suppression+recréation obligatoire. → `DoseDialog`
|
||
create/edit (préfill, changement de traitement, date/heure, notes, **override
|
||
d'ester par injection**), appelé depuis DosesScreen (tap sur la ligne) ;
|
||
`LogDoseDialog` supprimé (attention : `formatDose` vivait dedans → déplacée
|
||
top-level dans `DoseDialog.kt`).
|
||
|
||
**Session v1.2.1/v1.2.2 (remontées utilisateur) :**
|
||
25. **Calibration mélangeant les périodes d'ester** — le fond du « 250–375 » :
|
||
un facteur unique par traitement comparait des labs valerate à des prédictions
|
||
enanthate → ratios aberrants → courbes gonflées. → calibration **par période**
|
||
(`computeEsterScaleFactors` + `scalePerEster`, cf §7.6) ; vérifié que l'état
|
||
d'équilibre EEn (accumulation ×2) est CORRECT — le « 375 » = calcul non calibré.
|
||
26. **Lab T en unité aberrante (« 38 pg/mL », faute de frappe)** : renvoyé brut il
|
||
écrasait l'axe T et rendait la courbe T invisible. → `convertTToNgMl` avec
|
||
branche défensive pg/mL ÷1000 (et µg/L).
|
||
27. **Pas d'édition des labs** → LabDialog create/edit + tap sur la ligne ;
|
||
formatLabValue (préserve les décimales, contrairement à formatDose).
|
||
|
||
**Session v1.2.3/push Gitea :**
|
||
28. **Commentaires Kotlin imbriqués** : `/**TFS**` dans un KDoc ouvre un bloc
|
||
`/**` JAMAIS fermé (Kotlin les imbrique !) → « Unclosed comment » → NE PAS
|
||
mettre `/**` dans un texte de doc ; écrire « EEn + TFS ».
|
||
29. **XML de test périmés après échec de compilation** : quand compile échoue,
|
||
les anciens XML test-results restent → on « corrige » des échecs fantômes.
|
||
Toujours vérifier que la COMPILATION passe (grep `^e:` du log) avant
|
||
d'analyser les résultats de tests.
|
||
30. **filter-branch non idempotent** : le scrub (45.0→44.0) a corrompu une
|
||
constante d'un test PUR écrit après coup (assert 0.45 vs 44/100). Règle :
|
||
les remplacements d'anonymisation doivent être idempotents (les valeurs de
|
||
remplacement ne re-matchent jamais les motifs) et les tests purs doivent
|
||
utiliser des constantes hors des motifs de scrub.
|
||
31. **Release script : préfixe « v »** — le tag git = `v1.2.3`, la CHANGELOG
|
||
titre `[1.2.3]` : l'extraction cherchait `[vX.Y.Z]` → fallback silencieux.
|
||
→ `scripts/gitea-release.py` (corps = section CHANGELOG, APK attaché).
|
||
|
||
34. **Import JSON impossible si l'app contient déjà des données** (v1.2.6) :
|
||
l'insertion en « ajout » avec les IDs du backup entrait en conflit
|
||
d'unicité dès la moindre donnée existante → import échouait. → mode
|
||
ÉCRASEMENT (effacement ordonné enfants→parents puis restauration) +
|
||
restauration du tConfig + reprogrammation des rappels.
|
||
37. **Upload d'asset Gitea instable** (v1.2.10) : le `?name=` du POST et le
|
||
PATCH de renommage peuvent être ignorés (asset au nom générique, APK
|
||
release disparu de la release). → `ensure_asset` dans le script :
|
||
upload + vérification nom/taille + retry PATCH + échec bruyant.
|
||
42. **Dialog « Nouveautés » à chaque réouverture** (v1.3.2, remontée) :
|
||
la version vue était mémorisée APRÈS l'affichage — si l'app est fermée
|
||
pendant que le dialog est ouvert, DataStore n'avait pas encore flushé
|
||
→ même changelog ressortait. → mémoriser la version vue AVANT
|
||
d'afficher (l'affichage est purement visuel, la mémoire est déjà
|
||
faite). Titre = BuildConfig.VERSION_NAME (version de L'APP) : si
|
||
l'APK installé est v1.3.0, le titre montre 1.3.0 même si le contenu
|
||
parle d'une version plus récente — installer le dernier APK.
|
||
40. **Toggle agenda toujours inopérant** (v1.3.2, remontée) : le FIX v1.3.1
|
||
(callback async) était correct mais insuffisant : (a) le switch
|
||
s'activait même SANS Posologie (l'événement RRULE aurait un INTERVAL
|
||
invalide) → désormais refusé avec feedback rouge ; (b) au save, la
|
||
permission était vérifiée via le flag `remembered` (peut être faux
|
||
après recomposition) → vérification RÉELLE via ContextCompat ;
|
||
(c) AppLog journalise chaque étape (permission demandée/accordée,
|
||
upsert, delete) pour le debug à distance.
|
||
39. **Toggle de l'agenda inopérant** (v1.3.1, remontée) : le résultat de la
|
||
permission arrive ASYNCHRONE — le test synchrone juste après `launch()`
|
||
était toujours faux → le switch rebondissait sans s'activer, sans
|
||
feedback. → le CALLBACK active le switch ; journalisation AppLog ;
|
||
leçon : ne jamais lire un état de permission juste après launch().
|
||
41. **Export des logs plantait l'app** (v1.3.2, remontée) : le launcher
|
||
CreateDocument avec écriture inline dans une coroutine IO non couverte
|
||
par le même pattern que l'export JSON. → réutilise BackupManager
|
||
(openOutputStream "wt") + journalisation AppLog à chaque étape.
|
||
Leçon : quand un pattern d'IO marche (export JSON), le réutiliser
|
||
tel quel plutôt que d'en écrire un nouveau.
|
||
38. **Piège lexicographique de tags/versions** (v1.3.0, publication
|
||
farewell) : `tag >= "v1.2.5"` en comparaison de CHAÎNES fait `v1.2.10 <
|
||
v1.2.5` ("1" < "5") → la boucle de publication a sauté v1.2.10. →
|
||
toujours comparer les versions STRUCTURÉEMENT (tuple numérique, cf
|
||
ChangelogHelper.isVersionNewer) ; même famille que le bug #22 (casse
|
||
EEn) : identifiants « presque pareils ». (Numéro corrigé en v1.3.3 :
|
||
l'entrée était dupliquée sous « #41 » alors que §2/§16 référencent #38.)
|
||
43. **Course entre uploads rapprochés** (v1.2.6→v1.3.2, confirmé 4 fois) :
|
||
publier les 2 APK par DEUX INVOCATIONS rapprochées de gitea-release.py
|
||
→ le second upload écrase le premier (APK release disparu de la
|
||
release). → `scripts/publish-release.py` : UNE invocation fait purge +
|
||
upload des 2 APK + vérification PAR TÉLÉCHARGEMENT de chacun (§16.bis).
|
||
36. **Sens des boutons de zoom inversé** (v1.2.10, retour utilisateur) :
|
||
le « + » dézoomait (fenêtre plus longue) et le « − » zoomait — contre
|
||
la convention carte. → « + » = zoom avant (fenêtre courte), « − » =
|
||
zoom arrière. Le pinch (écarter = zoom avant) était déjà correct.
|
||
35. **Prévision cassée après un oubli d'injection** (v1.2.7) : le premier
|
||
créneau projeté tombait dans le passé (dernière + intervalle = jour
|
||
manqué) → faux pic dans l'historique + rythme décalé. → les créneaux
|
||
passés sont sautés (`while (t <= nowMs) t += intervalMs`), la prévision
|
||
démarre au premier créneau futur ; un retard décale toute la prévision
|
||
(comportement voulu, testé). NB : la doc §7.3b affirmait « strictement
|
||
après nowMs » alors que le code incluait les créneaux passés —
|
||
mismatch doc-code détecté par l'utilisatrice.
|
||
33. **Double insets edge-to-edge** (v1.2.5) : espace vide en haut de l'écran —
|
||
la fenêtre AppCompat poussait le contenu sous la barre de statut ET les
|
||
TopAppBar M3 rajoutaient leur padding interne. → `enableEdgeToEdge()` +
|
||
`contentWindowInsets = 0` sur les Scaffold, insets consommés une seule fois.
|
||
32. **`isActive` traité comme filtre de données** (v1.2.4) : passer un
|
||
traitement à inactif effaçait sa simulation du graphique (écrans
|
||
collectaient `activeTreatments`) tout en permettant d'y loger des doses.
|
||
→ sémantique documentée §6.bis : l'inactivation ne touche que la SAISIE
|
||
(chips, dropdown création, rappels) ; simulation et calibration reçoivent
|
||
TOUS les traitements. Épinglé par la régression n°3.
|
||
|
||
**Session v1.3.3 (audit de reprise de maintenance — session IA démarrant sans
|
||
aucun contexte, dans l'esprit de cette doc) :**
|
||
|
||
44. **Permissions agenda absentes du manifest** (v1.3.0 → v1.3.2) : la
|
||
fonctionnalité « événement d'agenda récurrent » ne pouvait JAMAIS
|
||
fonctionner — `WRITE_CALENDAR`/`READ_CALENDAR` n'étaient **pas déclarées
|
||
dans AndroidManifest.xml** (la demande limitée à l'exécution est refusée
|
||
d'office sans déclaration, et `CalendarEvents.ensureCalendar/upsert`
|
||
lèvent SecurityException). La doc §9 les listait pourtant depuis v1.3.0
|
||
→ leçon : « documenté » ≠ « implémenté » ; toute affirmation « le
|
||
manifest contient X » doit être vérifiée dans le XML réel (30 s de
|
||
`grep uses-permission`). Les fixes v1.3.1/v1.3.2 du « toggle agenda »
|
||
réparaient la logique UI mais pas la cause racine. FIX : déclaration +
|
||
commentaire dans le manifest.
|
||
45. **Export des logs plantait TOUJOURS l'app** (le « fix » v1.3.2, #41
|
||
plus haut, était insuffisant) : l'IO restait réimplémentée inline
|
||
(leçon du #41 : réutiliser BackupManager — PAS appliquée) et 3 appels
|
||
`AppLog.log` du callback étaient HORS try/catch — or une exception non
|
||
interceptée dans une coroutine à scope racine (`CoroutineScope
|
||
(Dispatchers.IO)`) remonte au handler de la thread = crash du process.
|
||
FIX : (a) l'export réutilise `BackupManager.writeBackup` TEL QUEL —
|
||
le même code path que l'export JSON qui marche sur le Pixel 9 /e/OS ;
|
||
(b) tout le callback est gardé ; (c) `AppLog.log` est fail-safe
|
||
(l'IO fichier est avalée dans le wrapper — une erreur disque ne doit
|
||
jamais tuer l'app pour une ligne de journal) ; (d) feedback visible
|
||
succès/échec + `logLineCount` rafraîchi (sur Main).
|
||
46. **Bump de version jamais commité** (v1.3.0 → v1.3.2) : les tags
|
||
v1.3.0/v1.3.1/v1.3.2 contenaient TOUS `versionCode = 14`
|
||
/ `versionName = "1.3.0"` → `BuildConfig.VERSION_NAME` était faux dans
|
||
les APK publiés (Paramètres, titre du dialog « Nouveautés »,
|
||
comparaison `isVersionNewer`). La checklist §16 étape 1 existait mais
|
||
n'a jamais été appliquée. FIX : versionCode 17 / « 1.3.3 » commité
|
||
AVANT le tag ; le bump fait partie du commit de release (jamais un
|
||
état local non commité au moment du build).
|
||
|
||
**Session v1.3.4 (deux crashs remontés → reproduits sur émulateur) :**
|
||
|
||
47. **L'écran Doses crashait l'app** (`MissingFormatArgumentException:
|
||
Format specifier '%3$d'`) : la string `hrt_duration` a TROIS placeholders
|
||
(`%1$d mois, %2$d jours, %3$d total`) mais
|
||
`stringResource(R.string.hrt_duration, months, days)` ne passait que DEUX
|
||
arguments. Crash dès que le fragment `totalDays > 0` s'affiche (donc pour
|
||
TOUTE donnée antérieure à aujourd'hui) — né en v1.3.1 (string + appel dans
|
||
le même commit fa5d2df, jamais testés ensemble) et passé à travers
|
||
v1.3.1→v1.3.3.
|
||
48. **Le bouton « Exporter » des logs crashait l'app** — LE bug récurrent des
|
||
v1.3.1→v1.3.3 : le nom de fichier était construit INLINE via
|
||
`LocalDate.now().format(ofPattern("yyyyMMdd-HHmm"))`. Un `LocalDate` n'a
|
||
PAS de champ horaire → `UnsupportedTemporalTypeException:
|
||
Unsupported field: HourOfDay` levée dans le onClick (thread principal,
|
||
synchrone, touch dispatch) → crash au tap, avant même le sélecteur SAF
|
||
(stack obtenue sur émulateur : LTSI + frames R8 obfusquées). L'export
|
||
JSON marchait car `yyyyMMdd` est un pattern valide pour LocalDate.
|
||
Zones d'ombre emboîtées : v1.3.2 et v1.3.3 « corrigent » l'IO (cf #41/#45)
|
||
sans voir cette ligne. FIX : `util/ExportFileNames.kt` (pur, testé,
|
||
documenté — couplage type↔pattern centralisé) ; `FileNamesTest` épinglant
|
||
les deux helpers (aurait attrapé le bug le jour même). Leçon : la
|
||
génération de nom de fichier ne doit JAMAIS vivre inline dans un onClick.
|
||
49. **StringFormatMatches** (capture du lint, v1.3.4) : la notification de
|
||
rappel passait un Double à `%s`. Fix `dose.toString()` (l'affichage ne
|
||
change pas). L'intérêt des règles lint est confirmé PAR CETTE SESSION :
|
||
`./gradlew lint` aurait signalé #47 dès v1.3.1 → le lint fait désormais
|
||
partie de la vérification avant release (les checks Compose 1.12+
|
||
théoriques `NonObservableLocale`/`LocalContextGetResourceValueCall` sont
|
||
tombés en warning via `app/lint.xml`, cf §19).
|
||
|
||
**Session v1.3.5 (diagnostic à distance via les logs exportés — la boucle par
|
||
l'exemple) :**
|
||
|
||
50. **Création du calendrier d'agenda impossible** (`IllegalArgumentException:
|
||
Sync adapters must specify an account and account type`, remontée par les
|
||
AppLog exportés du Pixel 9 v1.3.4) : `CalendarEvents.ensureCalendar`
|
||
construisait l'URI calendars avec `CALLER_IS_SYNCADAPTER=true` SANS
|
||
`ACCOUNT_NAME`+`ACCOUNT_TYPE` — requis par le CalendarProvider pour TOUTE
|
||
opération sync-adapter (requête ET insertion). De plus :
|
||
- les mêmes logs CONFIRMAIENT au passage les fixes précédents : export
|
||
logs `ok=true` (#48 ✓ depuis v1.3.4 au téléphone), permission agenda
|
||
accordée (manifest v1.3.3 ✓) — AppLog fait son job de diagnostic à
|
||
distance ;
|
||
- le log « événement agenda supprimé » était imprimé aussi quand le
|
||
switch était inéligible (posologie absente, permission absente,
|
||
switch off) → trois messages distincts désormais (créé-mis à jour /
|
||
supprimé / non activé) ;
|
||
- contexte : les « événement agenda supprimé » répétés dans les logs =
|
||
des saves où le switch ne remplissait pas les conditions d'activation
|
||
(l'utilisatrice avait retourné le switch après les échecs) — les trois
|
||
messages distincts rendront les prochains logs univoques.
|
||
FIX : append ACCOUNT_NAME/ACCOUNT_TYPE sur l'URI ; le pattern complet :
|
||
`syncadapter=true` + account + account_type sur les URIs de CRÉATION
|
||
(calendriers) ; l'insertion d'EVENTS l'avait déjà (upsertEvent). Validé
|
||
émulateur : `content query --uri content://com.android.calendar/calendars`
|
||
montre le calendrier, events avec RRULE corrects.
|
||
50.bis. **Delete d'événement silencieusement manqué** (cité au #50) : le même
|
||
défaut (CALLER_IS_SYNCADAPTER sans account) sur l'URI de DELETE de
|
||
`deleteEvent` — l'exception est avalée par le catch, le log dit
|
||
« supprimé » mais l'event reste dans le provider. FIX : mêmes params
|
||
sur l'URI. Règle : TOUT URI avec CALLER_IS_SYNCADAPTER doit embarquer
|
||
account+account_type — pas seulement les insertions.
|
||
51. **Événement orphelin après re-save** (découvert en validant #50 sur
|
||
émulateur — le provider montrait un event que la base ne référençait
|
||
plus) : `buildTreatment()` reconstruisait le Treatment SANS le champ
|
||
`calendarEventId` → null à chaque save → l'event existant devenait
|
||
orphelin dès la deuxième sauvegarde, plus jamais désactivable. FIX :
|
||
`loadedCalendarEventId` chargé dans le LaunchedEffect + renvoyé par
|
||
buildTreatment(). Leçon : la reconstruction complete d'une entité pour
|
||
un save doit repartir des champs NON ÉDITABLES (voir `loadedCreatedAt`,
|
||
déjà protégé, et désormais `loadedCalendarEventId`).
|
||
|
||
**Session v1.4.0 (modèle TFS sur la méta-analyse + fix #52) :**
|
||
|
||
53. **Delta « vs il y a 6 h » = en réalité 24 h** (v1.4.3, remontée) :
|
||
`NowLevelCard` prenait `curve.firstOrNull { écart ≥ 6 h }` sur une
|
||
fenêtre de 24 h — le premier point satisfait la condition
|
||
immédiatement → comparaison à ~24 h. Le lendemain d'une injection EEn
|
||
(plateau), ce delta 24 h peut être légèrement négatif → fausse
|
||
impression que « l'injection ne fait rien ». FIX : `pointHoursBefore`
|
||
(point au plus petit écart ≥ 6 h, testé). Leçon : un `firstOrNull` sur
|
||
une condition de distance trouve l'élément le PLUS LOIN, jamais le plus
|
||
proche — écrire l'intention (« le plus proche de N ») explicitement.
|
||
|
||
52. **Rappels quotidiens même hors jour d'injection** (remontée utilisateur :
|
||
« je m'injecte tous les samedis à 18 h → je dois recevoir le rappel
|
||
uniquement le samedi à 18 h ») : `nextReminderFireMs` ignorait doses et
|
||
Posologie → l'alarme se ré-armait chaque jour à HH:mm. FIX : le prochain
|
||
déclenchement suit la GRILLE (dernière dose + k × intervalle, créneaux
|
||
passés sautés — même sémantique que la prévision) à l'heure de rappel
|
||
choisie ; fallback quotidien sans Posologie/sans doses ; **et** la
|
||
chaîne one-shot est recousue après notif ET après « Pris » (avant :
|
||
seulement au boot/save). Leçon : toute logique de scheduling doit vivre
|
||
au MOTEUR (testable JVM), pas dans les composants Android.
|
||
|
||
**Leçons** : (a) ne jamais croire un build « probablement bon » sans l'avoir lancé ;
|
||
(b) les tests sémantiques attrapent ce que la compilation ne voit pas ; (c) se méfier des
|
||
constantes stdlib « de mémoire » (`ln2`), des mélanges Float/Double, et des APIs M3
|
||
expérimentales sans `@OptIn` ; (d) **un test de régression sur les VRAIES données
|
||
utilisateur** attrape les bugs de convention (casse, unités) que les tests
|
||
synthétiques ratent — mais garde ces données **hors du dépôt** (§8.bis) ;
|
||
(e) attention aux identifiants « presque pareils » entre sources (constantes app
|
||
vs clés d'asset) ; (f) après un échec de COMPILATION, jeter les résultats de tests
|
||
de la même passe (XML périmés) ; (g) tout script de réécriture d'historique doit
|
||
être idempotent ; (h) dans un KDoc, `/**` imbrique ; (i) quand un pattern d'IO
|
||
marche (export JSON), le réutiliser TEL QUEL — une réimplémentation « équivalente »
|
||
perd les garde-fous acquis à l'usage (cf #45) ; (j) vérifier les POSTULATS dans le
|
||
code réel, pas dans la doc (« le manifest contient WRITE_CALENDAR », cf #44) ;
|
||
(k) un bump de version non commité = métadonnées fausses dans les APK publiés
|
||
(cf #46) — le bump fait partie du commit de release ; (l) le scan de
|
||
confidentialité s'ADAPTE automatiquement aux nouveaux exports
|
||
(`local-test-data/`) : un historique « propre hier » peut devenir hit dès
|
||
qu'un nouvel export introduit des motifs qui collent — re-scanner PUIS juger
|
||
avec l'utilisatrice (dette déjà jugée, cf §8.bis) ; (m) **un crash sans stack
|
||
trace = d'abord le reproduire** (émulateur + données réelles, §16.ter) — les
|
||
deux « fixes » aveugles de v1.3.2/v1.3.3 (#41/#45) ont laissé passer un bug
|
||
trivial (#48) seulement visible sur l'émulateur ; (n) `./gradlew lint` fait
|
||
désormais partie de la vérification avant release : `StringFormatMatches`
|
||
aurait signalé #47 dès v1.3.1.
|
||
|
||
## 15. Comment régénérer l'asset pk_profiles.json
|
||
|
||
Si le `.ods` change (re-fits, nouveaux esters) :
|
||
|
||
```python
|
||
# python3 stdlib only :
|
||
# 1. zipfile.ZipFile(ods).read("content.xml")
|
||
# 2. ElementTree (ns table/office/text) → table "Models"
|
||
# 3. lignes 1-4 = D, k1, k2, k3 (colonnes EV/EU/EEn ese + tfs) — informatif, non utilisé
|
||
# 4. lignes 5+ = profils horaires (00:00 … 8000:00), décimaux FR "61,12" → float
|
||
# 5. json.dump({"params": …, "profiles": {"EV_ese": [8001], "EU_ese": …, "EEn_ese": …,
|
||
# "EV_tfs": …, "EU_tfs": …, "EEn_tfs": …}})
|
||
# 6. cp vers app/src/main/assets/pk_profiles.json
|
||
# 7. vérifier : 6 clés × 8001 valeurs, pics == référence (§7.1) ; les tests le vérifient
|
||
```
|
||
|
||
Le script de la session 1 a été exécuté inline (non archivé) — le refaire depuis la
|
||
structure ci-dessus. **Toute restructuration du JSON impose de mettre à jour
|
||
`PKProfileStore.initWithJson`.**
|
||
|
||
## 16. Workflow build / test / install / git
|
||
|
||
```bash
|
||
cd ~/projects/HormoneTrack
|
||
./gradlew assembleDebug testDebugUnitTest # build + 87 tests (44 sans données locales)
|
||
./gradlew assembleRelease # APK optimisé R8 (cf §16.bis)
|
||
adb install -r app/build/outputs/apk/debug/app-debug.apk
|
||
```
|
||
|
||
**Git (initialisé le 2026-09-05, branche `main`, DEUX remotes Gitea)** :
|
||
- `origin` → `https://gitea.cloudyfy.fr/Siphonight/HormoneTrack` (privé, HTTPS + trousseau)
|
||
- `farewell` → `git@farewell:Siphonight/HormoneTrack.git` (SSH, alias `farewell`
|
||
= `giteassh.farewell.dev:2222` avec clé dédiée, cf `~/.ssh/config`) —
|
||
**repo créé + push + LES 12 RELEASES publiées avec APK vérifiés par
|
||
téléchargement (2026-09-06)** ; token `gitea.farewell.dev` au trousseau
|
||
(scope write:repository)
|
||
- Historique = commits logiques par couche (toolchain / moteur / UI / docs) ;
|
||
- **Chaque release = tag annoté** (`v1.1.0`, `v1.2.0`, …) :
|
||
`git tag -a vX.Y.Z -m "…" && git tag` pour lister ;
|
||
- `local.properties`, `build/`, `.gradle/`, `.idea/`, **`local-test-data/`** sont
|
||
ignorés (`.gitignore`) — ce dernier protège les données de santé de test ;
|
||
- Avant chaque commit de release : `./gradlew testDebugUnitTest` doit être vert ;
|
||
- Push : `git push origin main --tags` + `git push farewell main --tags`
|
||
(farewell : auth SSH par clé dédiée ; le push-to-create y est
|
||
DÉSACTIVÉ → le repo doit exister au préalable sur l'instance).
|
||
⚠️ Tokens Gitea : scope `write:repository` suffit pour push, releases et
|
||
assets ; il NE permet PAS de créer un repo via API (il faut `write:user`)
|
||
ni d'utiliser push-to-create.
|
||
|
||
### 16.bis Releases Gitea avec APK téléchargeable
|
||
|
||
Les releases Gitea embarquent l'APK de chaque version — les utilisateurs n'ont pas
|
||
besoin de compiler. **Le corps de chaque release = la section CHANGELOG de la
|
||
version** (markdown rendu par Gitea), extrait automatiquement.
|
||
|
||
Le tout est automatisé par `scripts/gitea-release.py` — **multi-instances**
|
||
(`cloudyfy` par défaut, `farewell`) :
|
||
|
||
```bash
|
||
# 0. Le CHANGELOG est embarqué dans l'app (dialog « Nouveautés ») :
|
||
# la tâche Gradle copyChangelog copie docs/CHANGELOG.md vers
|
||
# src/main/assets/changelog.md à chaque build (gitignoré, auto)
|
||
|
||
# 1. Construire les DEUX APK au niveau du tag (debug + release R8)
|
||
git checkout vX.Y.Z
|
||
./gradlew assembleRelease assembleDebug # release = recommandé (R8, cf plus bas)
|
||
cp app/build/outputs/apk/release/app-release.apk /tmp/apks/HormoneTrack-vX.Y.Z-release.apk
|
||
cp app/build/outputs/apk/debug/app-debug.apk /tmp/apks/HormoneTrack-vX.Y.Z-debug.apk
|
||
git checkout main
|
||
|
||
# 2. Publier sur chaque instance (corps = CHANGELOG + les 2 APK attachés)
|
||
# ⚠️ NE JAMAIS comparer les tags en chaînes (v1.2.10 < v1.2.5
|
||
# lexicographiquement !) — comparer en tuples numériques (§14 #38)
|
||
# ⚠️ NE JAMAIS utiliser gitea-release.py en deux invocations rapprochées
|
||
# (les uploads se remplacent mutuellement, confirmé 4 fois) :
|
||
# → UNE invocation de **scripts/publish-release.py** par instance fait
|
||
# tout (purge + upload des 2 APK + vérification par téléchargement).
|
||
python3 scripts/publish-release.py cloudyfy vX.Y.Z
|
||
python3 scripts/publish-release.py farewell vX.Y.Z
|
||
```
|
||
|
||
Le script choisit l'instance (URL, owner) et lit le token Gitea correspondant
|
||
dans le trousseau macOS (`security find-internet-password -s <hôte> -w`) :
|
||
`gitea.cloudyfy.fr` ✓ présent ; `gitea.farewell.dev` → à ajouter :
|
||
`security add-internet-password -s gitea.farewell.dev -a Siphonight -w <TOKEN> -U`.
|
||
|
||
Le script :
|
||
- extrait la section `## [X.Y.Z]` de `docs/CHANGELOG.md` comme corps ;
|
||
⚠️ piège : le tag git porte le « v » (`v1.2.3`) mais la CHANGELOG non
|
||
(`[1.2.3]`) — première version du script cherchait `[vX.Y.Z]` et tombait sur
|
||
le fallback « Voir docs/CHANGELOG.md » ;
|
||
- crée la release si absente, sinon **met à jour le corps** (PATCH) ;
|
||
- attache l'APK (remplace l'asset du même nom si présent) — appeler le script
|
||
DEUX FOIS pour publier les deux APK (noms distincts) ;
|
||
⚠️ vérification intégrée (`ensure_asset`) : le `?name=` de l'upload et/ou
|
||
le PATCH peuvent être ignorés par les instances (assets au nom générique /
|
||
APK disparu — v1.2.6, v1.2.10) → le script vérifie **nom ET taille** après
|
||
chaque upload, retente le PATCH une fois, et **échoue bruyamment** si
|
||
l'asset ne colle pas — ne pas retirer ;
|
||
- **Releases antérieures à v1.2.5 = APK debug unique** : backfill des APK
|
||
release abandonné (best effort) — les anciens `build.gradle.kts` sortaient
|
||
un `app-release-unsigned.apk` non signé (pas de signing config à l'époque).
|
||
Les utilisateurs prennent la dernière version.
|
||
`HormoneTrack-vX.Y.Z-release.apk` (**recommandé**, R8) et
|
||
`HormoneTrack-vX.Y.Z-debug.apk` ;
|
||
- lit le token Gitea dans le trousseau macOS (`security find-internet-password`) ;
|
||
scope requis : `write:repository` (suffit pour releases + assets, pas pour
|
||
créer un repo — cf plus haut).
|
||
|
||
**Build release (v1.2.5)** : `./gradlew assembleRelease` — R8 full mode +
|
||
`isShrinkResources` + signé avec la **clé debug** (même signature que les APK
|
||
debug distribués → mise à jour par-dessus sans perte de données ; 20 Mo →
|
||
2,4 Mo). Garde-fous dans `proguard-rules.pro` : Gson lit les champs des
|
||
modèles par réflexion → `-keep` explicites sur `data.model.**`,
|
||
`BackupData`, `TConfig` (+ attributs `Signature`) — sinon l'export/import
|
||
JSON casse UNIQUEMENT en release. ⚠️ À chaque activation d'optimisation :
|
||
tester sur téléphone l'export/import de backup + les graphiques (R8 ne se
|
||
vérifie pas en tests JVM).
|
||
|
||
### Checklist de déploiement complète (de A à Z, pour une session sans contexte)
|
||
|
||
1. **Bumper la version** dans `app/build.gradle.kts` : `versionCode = N+1`,
|
||
`versionName = "X.Y.Z+1"` (SemVer : fix = Z, feature = Y).
|
||
2. **Tests verts obligatoires** : `./gradlew testDebugUnitTest` — 137 au
|
||
total, 115 si `local-test-data/` est absent (les 3 classes de régression
|
||
réelles sont skippées via `Assume`, 6 tests chacune) ; **lint vert
|
||
obligatoire** : `./gradlew lint` (v1.3.4 — `StringFormatMatches` aurait
|
||
attrapé les crashs #47/#48 dès v1.3.1). Paliers de test du projet :
|
||
chaque commit = tests + lint ; release = + scan confidentialité + APK au
|
||
tag ; crash reporté = reproduction émulateur (§16.ter) AVANT tout fix.
|
||
3. **Docs** : section `## [X.Y.Z]` en tête de `docs/CHANGELOG.md` (le corps des
|
||
releases Gitea en sera extrait automatiquement par le script), + §14 si bug
|
||
corrigé, + §2 (historique) si notable.
|
||
4. **Commit** (message descriptif par couche) + **tag annoté** :
|
||
`git tag -a vX.Y.Z -m "résumé"`.
|
||
5. **Vérif confidentialité** : `git ls-tree -r <tag> --name-only | grep local-test-data` → VIDE + scan de confidentialité (déjà exigé avant tout push, §8.bis) ; puis **Push** : `git push origin main --tags` + `git push farewell main --tags`
|
||
(farewell : le repo doit exister sur l'instance ; push-to-create désactivé).
|
||
6. **Construire les 2 APK au niveau du tag** :
|
||
`git checkout vX.Y.Z` → `./gradlew assembleRelease assembleDebug` → copier
|
||
`app-release.apk` → `/tmp/apks/HormoneTrack-vX.Y.Z-release.apk` et
|
||
`app-debug.apk` → `HormoneTrack-vX.Y.Z-debug.apk` → `git checkout main`.
|
||
7. **Publier les releases** (corps = section CHANGELOG + les 2 APK attachés) :
|
||
`python3 scripts/publish-release.py cloudyfy vX.Y.Z` puis idem avec
|
||
`farewell` (⚠️ UNE invocation = les 2 APK + vérification par
|
||
téléchargement — NE PAS utiliser gitea-release.py deux fois de suite,
|
||
les uploads rapprochés se remplacent mutuellement, cf §14 #43 ;
|
||
token `gitea.farewell.dev` requis dans le trousseau).
|
||
8. **Smoke-test R8 sur téléphone** (le release APK n'est pas vérifiable en
|
||
tests JVM) : installation par-dessus l'existant, graphiques, export ET
|
||
import d'un backup JSON, rappel. Le debug APK est le repli (même signature).
|
||
9. **Mettre à jour §2/§8/§19/§21 si besoin** puis pusher la doc.
|
||
|
||
Notes :
|
||
- L'APK est une **build debug** signée avec la clé debug locale — installable en
|
||
sideload, mises à jour entre versions OK (même signature) ;
|
||
- Le dépôt est **privé** : le téléchargement des releases exige d'être connecté ;
|
||
rendre le dépôt public rend les APK téléchargeables sans compte (aucune donnée
|
||
de santé dans le dépôt, cf §8.bis).
|
||
|
||
- Téléphone : mode développeur + Débogage USB (détails : GUIDE_INSTALLATION.md)
|
||
- À ma charge (assistant) : build + tests JVM ✓ ; **émulateur installé et
|
||
opérationnel depuis v1.3.4 (recette §16.ter)** ;
|
||
**les tests humains sur vrai téléphone restent la référence**
|
||
(notifs → montre, UX de saisie, pickers, panoramique du chart)
|
||
|
||
### 16.ter Recette : test manuel sur émulateur (reproductible — v1.3.4)
|
||
|
||
But : reproduire/valider un crash ou une UI sur l'app RÉELLE (sans téléphone
|
||
branché) — c'est cette recette qui a diagnostiqué les crashs #47/#48 en une
|
||
session. ⚠️ AUCUNE donnée personnelle n'est embarquée : le backup utilisé vit
|
||
sous `local-test-data/` (gitignoré, §8.bis) et est passé au script en argument.
|
||
|
||
**0. Installation (one-shot, cf §4)** : `sdkmanager "emulator"
|
||
"system-images;android-31;aosp_atd;arm64-v8a"` (léger, pour le LOGIQUE sans
|
||
SAF) et/ou `"system-images;android-36;google_apis;arm64-v8a"` (image COMPLÈTE,
|
||
nécessaire pour piloter les sélecteurs de fichiers). AVD :
|
||
`avdmanager create avd -n hrt36 -k "system-images;…" -d pixel_6` (l'erreur
|
||
béninie « devices.xml » ne bloque pas la création). Boot headless :
|
||
`emulator -avd hrt36 -no-window -no-audio -no-boot-anim -no-snapshot
|
||
-gpu swiftshader_indirect &` puis `adb wait-for-device` + attente
|
||
`getprop sys.boot_completed`.
|
||
|
||
**1. Seed des « données réelles »** — la DB est modifiable uniquement sur un
|
||
APK DEBUGGABLE (`run-as`) :
|
||
|
||
```bash
|
||
adb install app/build/outputs/apk/debug/app-debug.apk
|
||
adb shell am start -n com.hormonetrack/.MainActivity # 1er lancement → crée la DB
|
||
adb shell am force-stop com.hormonetrack # flush de la DB
|
||
adb shell run-as com.hormonetrack cat databases/hormonetrack.db > /tmp/hrt.db
|
||
python3 scripts/seed-emulator.py /tmp/hrt.db local-test-data/backup-v1.3.1.json
|
||
adb push /tmp/hrt.db /data/local/tmp/hrt.db
|
||
adb shell run-as com.hormonetrack cp /data/local/tmp/hrt.db databases/hormonetrack.db
|
||
adb shell run-as com.hormonetrack rm -f databases/hormonetrack.db-wal \
|
||
databases/hormonetrack.db-shm # WAL périmé sinon ! (checkpoint implicite fait par le seed)
|
||
```
|
||
|
||
Règles du seed (le script les impose) : colonnes = noms de propriétés Kotlin
|
||
(Room n'applique pas de snake_case), enums stockés en String — on insère
|
||
SEULEMENT des lignes, on ne touche NI au schéma NI à `room_master_table`
|
||
(Room valide le schéma à l'ouverture, pas les lignes).
|
||
|
||
**2. Passer en RELEASE (c'est LE build à tester)** — `install -r` PAR-DESSUS
|
||
le debug CONSERVE la DB (les APK release/signés debug sont la même clé) :
|
||
|
||
```bash
|
||
adb install -r app/build/outputs/apk/release/app-release.apk
|
||
adb logcat -c && adb shell am start -n com.hormonetrack/.MainActivity
|
||
```
|
||
|
||
**3. Piloter l'UI headless** : à l'aide de `uiautomator dump` + `input tap` —
|
||
l'UI se pilote SANS écran :
|
||
|
||
```bash
|
||
adb shell uiautomator dump /sdcard/ui.xml && adb shell cat /sdcard/ui.xml
|
||
# → parser text=…/content-desc=… + bounds=[x1,y1][x2,y2] → centre (cx,cy)
|
||
adb shell input tap <cx> <cy> # naviguer (5 tabs ≈ y=2280 à 1080×2400)
|
||
adb shell input keyevent KEYCODE_BACK
|
||
```
|
||
|
||
**4. Attraper le crash** : le process meurt et repasse au launcher — c'est le
|
||
signal ; stack : `adb logcat -b crash -d` (grep FATAL). **0 crash attendu :
|
||
`grep -c FATAL` reste à 0** après chaque interaction.
|
||
|
||
**5. Flux SAF (export JSON/logs)** : sur android-31 ATD le
|
||
DocumentsUI est STUBBÉ (`fakesystemapp`) → INUTILISABLE ; utiliser l'image
|
||
android-36 google_apis. Le picker se pilote pareil (bouton `SAVE` en bas à
|
||
droite) ; vérification : fichier présent dans `/sdcard/Download/` + le message
|
||
de confirmation (v1.3.3 : « Logs de diagnostic exportés ») visible via un
|
||
nouveau dump UI.
|
||
|
||
**6. Vérifications post-fix** : Doses (en-tête « temps sous THS » + liste),
|
||
Settings → Export logs (SAF + SAVE + message), export/import JSON, changelog
|
||
dialog au premier lancement (sinon re-seed un `changelog_seen_version`).
|
||
⚠️ Les coordonnées UI (x/y) évoluent avec l'écran/l'app : TOUJOURS re-dumper
|
||
`uiautomator` avant de taper — ne jamais figer des coordonnées hors dump.
|
||
|
||
## 17. Montre : Gadgetbridge & options
|
||
|
||
Doc dédiée : [MONTRE-GADGETBRIDGE.md](MONTRE-GADGETBRIDGE.md). Synthèse :
|
||
|
||
- GT 3 = Lite Wearable ; GB supporte la GT 3 (« mostly supported ») : **notifications ✓,
|
||
watchfaces `.hwt` ✓, apps `.hap` ✗**
|
||
- Health et GB ne peuvent pas être appairés simultanément
|
||
- Watchface via GB : **aucune signature requise** ; app `.hap` : certificat debug AGC +
|
||
UDID (chaîne DevEco Studio → DevEco Assistant)
|
||
- Régression connue : HarmonyOS 6.1+ casse l'install `.hwt` via GB (issues #5968/#6005/#6199) ;
|
||
GT 3 en HarmonyOS 4.0.0.120 probablement OK, à valider
|
||
- Choix v1 : notifications via GB/Health ; Phase 2 : watchface custom (statique) ou
|
||
mini-app Lite Wearable autonome (Wear Engine = accès partenaire)
|
||
|
||
## 18. Espace disque & coûts
|
||
|
||
Mesuré le 5 sept. 2026 (Mac, 228 Go, **33 Go libres** au départ) :
|
||
|
||
| Élément | Taille |
|
||
|---|---|
|
||
| SDK Android (cmdline-tools + platforms 34/36/**37** + build-tools 34/36/37 + platform-tools) | ≈ 700 MB |
|
||
| Cache Gradle (~/.gradle, plusieurs distributions 8.9→9.7.1 + deps AGP 9/Compose 1.12) | ≈ 3–4 GB |
|
||
| Projet (sources + build outputs) | ≈ 100 MB |
|
||
| **Total outillage actuel** | **≈ 4–5 GB** |
|
||
|
||
Marges : émulateur + image système ≈ +2–3 GB ; DevEco Studio (Phase 2) ≈ +10 GB →
|
||
tout rentre très largement. Note : **AGP télécharge automatiquement les plateformes
|
||
manquantes** (licences signées) — c'est comme ça que android-37 est arrivé.
|
||
|
||
## 19. Limites connues
|
||
|
||
Volontaires (v1) :
|
||
- Pas de ViewModel/DI (couplage UI↔repo via CompositionLocal)
|
||
- Modèle T empirique (non publié) — étiqueté estimation partout
|
||
- Import JSON = **écrasement** depuis v1.2.6 (mode fusion non implémenté)
|
||
- Profils par **tables** (pas par formule) : les D/k1–k3 de l'ODS ne sont pas consommés —
|
||
rétro-ingénierie des fits non tentée ; les tables sont exactes
|
||
- DST : les rappels quotidiens peuvent glisser d'1 h après changement d'heure, jusqu'au
|
||
prochain reschedule (boot/save) — mineur
|
||
- Labs : marqueur libre — E2/T exacts requis pour calibration/charts
|
||
- `allowBackup=false` → seul backup = export JSON manuel
|
||
- **APK release (R8)** : la réflexion Gson est couverte par des `-keep`
|
||
explicites, mais R8 ne se vérifie pas en tests JVM → **smoke-test sur
|
||
téléphone** (export/import backup, graphiques) avant chaque publication ;
|
||
signé clé debug → upgradable sans perte, mais pas une signature « officielle »
|
||
- **Grille de rappel sous-quotidienne** (v1.4.0) : une Posologie < 1 j
|
||
(ex. 2 injections/jour) + UNE heure de rappel → l'app ne peut exprimer
|
||
qu'un rappel/jour à heure fixe les jours de créneau (épinglé par
|
||
`ReminderScheduleTest`) ; deux heures de rappel distinctes = évolution
|
||
- **Lint (v1.3.4)** : `./gradlew lint` est vert — `StringFormatMatches` et
|
||
famille restent des ERREURS bloquantes ; les checks Compose 1.12+
|
||
théoriques (`NonObservableLocale`, `LocalContextGetResourceValueCall`,
|
||
staleness de configuration) sont rétrogradés en WARNING via `app/lint.xml`
|
||
→ à re-traiter lors d'une refonte i18n (§20)
|
||
|
||
## 20. Idées d'évolution
|
||
|
||
1. **Robolectric + tests Compose** (VM Android en JVM — pas besoin d'appareil)
|
||
2. **Émulateur local** pour smoke-tests UI (sur demande, ~2–3 Go)
|
||
3. Mode « planifier les injections » (schedule récurrent → pré-remplir le log)
|
||
4. Import JSON : détection de doublons / mode fusion optionnel (l'écrasement est fait, v1.2.6)
|
||
5. Verrou biométrique (BiometricPrompt), widget, export CSV
|
||
6. Charts : tooltip au toucher (le pan est fait v1.2.0, **le zoom v1.2.9**) ;
|
||
MaterialExpressiveTheme quand l'API passera publique (cf §3)
|
||
7. **Vrai keystore de distribution** (signature dédiée ≠ clé debug → nécessite
|
||
une migration : backup → désinstallation → installation signée → réimport)
|
||
|
||
Fait (à ne pas refaire) : pan du chart (v1.2.0), **zoom du chart (v1.2.9)**,
|
||
pics/creux (v1.2.3), prévision par fréquence (v1.2.0), calibration par période
|
||
d'ester E2 **et** T (v1.2.1/v1.2.3), édition doses (v1.1.0) et labs (v1.2.2),
|
||
E2+T en une entrée (v1.2.2), migration Room v1→v2 sans fallback destructif
|
||
(v1.2.0), rooms v2→v3 (calendarEventId, v1.3.0), dépôt Gitea + releases APK
|
||
(push session).
|
||
8. Phase 2 montre : watchface `.hwt` custom, puis mini-app Lite Wearable (cf §17)
|
||
|
||
## 21. Checklist de test manuel
|
||
|
||
Sur le téléphone de test (à compléter par l'utilisatrice) :
|
||
|
||
- [ ] App se lance sans crash (asset chargé — sinon cf §14.4)
|
||
- [ ] Créer traitement « EV — Estrannaise » 4 mg + rappel 2 min à l'avance
|
||
- [ ] Notif arrive sur le téléphone **et** la GT 3 (via GB ou Health)
|
||
- [ ] « Pris » → dose loguée dans Doses ; « Reporter 1 h » → nouvelle notif 1 h après
|
||
- [ ] Logger 2–3 injections passées → Home affiche E2/T + delta 6 h cohérents
|
||
(4 mg EV → pic ≈ 4×61×scale ≈ 244 pg/mL à scale=1)
|
||
- [ ] Ajouter un lab E2 → « Calibrer avec les analyses » → scaleFactor plausible (0,5–1,2)
|
||
- [ ] Labs T + « Calibrer k » → k mis à jour, courbe T proche des points
|
||
- [ ] Charts 24 h/7 j/30 j, toggles T/labs, axes lisibles
|
||
- [ ] Export JSON → fichier inspectable ; ré-import → compteur correct
|
||
- [ ] Langue FR↔EN↔Système : UI + notifs basculent
|
||
- [ ] Redémarrer le téléphone → rappel reprogrammé (BootReceiver)
|
||
- [ ] Désactiver un rappel → plus de notif (cancel — cf §14.3)
|
||
- [ ] Tester l'installation d'une watchface `.hwt` via Gadgetbridge (pour la Phase 2)
|
||
- [ ] **v1.2.x** : graphique panoramique (glisser → passé, bouton « Revenir à maintenant »)
|
||
- [ ] **v1.2.x** : toggles Estrannaise/TFS indépendants (les deux courbes superposées)
|
||
- [ ] **v1.2.x** : chip « Prévision » (configurer la Posologie d'un traitement d'abord)
|
||
- [ ] **v1.2.x** : chip « Pics / creux » (triangles ▲▼ aux extrema E2 et T)
|
||
- [ ] **v1.2.x** : Calibration automatique ON → courbes ajustées depuis les labs
|
||
(par période d'ester si changement d'ester), OFF → valeurs stockées
|
||
- [ ] **v1.2.x** : tap sur le mini-chart de l'accueil → écran Graphiques
|
||
- [ ] **v1.2.x** : édition d'une dose (tap ligne Doses) et d'un lab (tap ligne
|
||
Analyses → sélecteur E2/T si paire) ; suppression par prise entière
|
||
- [ ] **v1.2.x** : prise de sang E2 + T en une entrée (champs optionnels)
|
||
- [ ] **v1.2.9** : zoom du graphique (pinch 2 doigts + boutons − / + ; labels
|
||
X adaptatifs ; courbes lisses à fort zoom)
|
||
- [ ] **v1.2.7** : prévision après un OUBLI (pas de faux pic dans le passé)
|
||
et après un RETARD (prévision décalée, suivant la dernière prise)
|
||
- [ ] **v1.2.6** : import d'un backup avec l'app déjà remplie → ÉCRASER &
|
||
restaurer (message clair), tConfig restauré, rappels reprogrammés
|
||
- [ ] **v1.2.4** : passer un traitement à inactif → retiré de « Log rapide » et
|
||
du dropdown des nouvelles doses, rappel annulé, **mais sa simulation reste
|
||
sur le graphique** et la calibration couvre toujours ses périodes
|
||
- [ ] **v1.3.3** : **export des logs** → le gestionnaire de fichiers s'ouvre,
|
||
le .txt est écrit, un message « Logs de diagnostic exportés » s'affiche ;
|
||
cas d'échec (annulation du chooser = silencieux, échec d'écriture =
|
||
message d'erreur, l'app ne doit JAMAIS planter — bug #45, Pixel 9 /e/OS)
|
||
- [ ] **v1.3.3** : **version affichée** « 1.3.3 » dans Paramètres et en titre du
|
||
dialog « Nouveautés » (bug #46 : les APK v1.3.0–1.3.2 affichaient 1.3.0) ;
|
||
au premier lancement de v1.3.3, le dialog liste 1.3.1→1.3.3 (dernière vue
|
||
mémorisée = « 1.3.0 » sur les anciens APK)
|
||
- [ ] **v1.3.3** : **événement d'agenda** : le switch demande la permission
|
||
agenda (cette fois le système doit vraiment montrer le dialogue de
|
||
permission — elle est désormais dans le manifest, bug #44) ; après
|
||
accord, l'événement récurrent apparaît dans l'app d'agenda du Pixel 9 ;
|
||
le smoke-test R8 de la checklist §16 doit aussi couvrir export/import
|
||
de backup + graphiques (réflexion Gson sous R8)
|
||
- [ ] **v1.3.4** : **écran Doses** s'ouvre et affiche l'en-tête « temps sous
|
||
THS » avec les données réelles (bug #47 : crash dès que des doses
|
||
antérieures à aujourd'hui existaient — né v1.3.1)
|
||
- [ ] **v1.3.4** : **export des logs** ouvre VRAIMENT le gestionnaire de
|
||
fichiers et le message « Logs de diagnostic exportés » apparaît après
|
||
la sauvegarde (bug #48 : crash au tap depuis v1.3.1) — vérifié sur
|
||
émulateur API 36 (§16.ter), à confirmer au téléphone
|
||
- [ ] **v1.3.4** : rappel de notification affiche la dose proprement
|
||
(lint #49, ex format « 4.0 »)
|
||
- [ ] **v1.3.4** : `./gradlew lint` vert — intégré à la vérification de
|
||
release (step 2 de la checklist §16 désormais : tests + lint)
|
||
- [ ] **v1.3.5** : **agenda end-to-end** : sur un traitement actif avec
|
||
Posologie, activer « Événement d'agenda récurrent » → permission
|
||
accordée → sauvegarder → l'événement existe dans le calendrier local
|
||
« HormoneTrack » (« créé/mis à jour (id=…) » dans les logs exportés) ;
|
||
puis **désactiver le switch → re-save → l'événement disparaît
|
||
RÉELLEMENT** de l'app d'agenda (bugs #50 / #50 bis / #51 : en v1.3.4
|
||
ni création ni suppression ne fonctionnaient, et les re-saves
|
||
orphelinaient les événements — cf §14)
|
||
- [ ] **v1.4.0** : **rappel = jour du créneau uniquement** (fix #52) : un
|
||
traitement avec Posologie 7 j + rappel 18 h ne doit sonner que le jour
|
||
d'injection à 18 h (pas les autres jours) ; la carte « Prochaine
|
||
dose » de l'accueil montre le même créneau ; après « Pris » dans la
|
||
notification, le rappel suivant se cale sur le créneau d'après
|
||
- [ ] **v1.4.0** : **courbes TFS V3C** : superposer Estrannaise/TFS sur un
|
||
traitement EV — les deux doivent être proches aux pics (58–61
|
||
pg/mL/mg) et diverger en queues (V3C = queues exactes) ; vérifier un
|
||
preset EB/EC/PEP (nouvelles courbes) ; PEP à 32,5 mg → niveaux
|
||
comparables à un EEn à 5 mg
|
||
- [ ] **v1.4.1** : **prévision sans saut** : ouvrir Graphiques → activer le
|
||
chip Prévision → le graphique NE BOUGE PAS (avant v1.4.1 il sautait
|
||
« tout à droite ») ; tirer vers la GAUCHE → la courbe projetée défile
|
||
vers le futur (jusqu'à 1 an selon la Posologie) ; « Revenir à
|
||
maintenant » ramène ; désactiver le chip dans le futur → retour auto
|
||
- [ ] **v1.4.1** : **carte « Prochaine dose » en jours** : avec un créneau
|
||
à > 24 h, l'accueil affiche « 5 j 2 h · sam. 6 18:00 (traitement) »
|
||
(et non « 122h22 ») ; à < 24 h, format heures/minutes inchangé
|
||
- [ ] **v1.4.2** : **seuils d'alerte** : Paramètres → « Seuils d'alerte » →
|
||
E2 haut 200 (avec un niveau estimé > 200) → Save → message « Saved » +
|
||
**notification immédiate** (canal dédié) + carte rouge sur l'accueil
|
||
(« ▲ E2 estimé ≈ X — au-dessus de ta limite (200) ») ; re-save sans
|
||
changer → PAS de nouvelle notif (anti-spam) ; seuil haute au-dessus du
|
||
niveau → tout disparaît ; validation : haut ≤ bas → message rouge
|
||
- [ ] **v1.4.2** : **backup avec paramètres** : Export JSON → le fichier
|
||
contient `"settings"` (langue, auto_calibrate, seuils) ; réinstaller +
|
||
Import → langue/seuils restaurés ; un VIEUX backup (v1.4.1) s'importe
|
||
sans les réglages (rétrocompat) ; smoke-test R8 : export/import
|
||
(réflexion Gson sur UserSettings, `-keep` ajouté)
|
||
- [ ] **v1.4.3** : **delta 6 h de l'accueil** : le lendemain d'une injection
|
||
EEn (plateau), la carte « niveau actuel » doit comparer à il y a 6 h
|
||
RÉELLEMENT (bug #53 : avant, la comparaison portait sur 24 h et le
|
||
delta pouvait être trompeusement négatif) — vérifier la cohérence avec
|
||
la courbe 24 h du graphique
|
||
|
||
---
|
||
*Doc mise à jour le 7 sept. 2026 (v1.4.3) — build OK, lint vert, 137/137 tests verts (115 sans
|
||
les données locales), dépôts Gitea (cloudyfy + farewell) avec releases APK, aucune donnée
|
||
de santé dans le dépôt ni l'historique. Fil des corrections : v1.3.3 = reprise de
|
||
maintenance (permissions agenda, export logs IO, bump de version) ; v1.3.4 = 2 crashs
|
||
reproduits sur émulateur (#47 stringResource arity, #48 LocalDate+pattern horaire), lint
|
||
filet bloquant ; v1.3.5 = diagnostic à distance via les logs exportés (#50/#50bis/#51
|
||
agenda) ; v1.4.0 = modèle TFS V3C sur la méta-analyse officielle (7 esters, fidélité
|
||
~1 % épinglée) + rappels sur la grille Posologie (#52) ; v1.4.1 = prévision étendue sans saut (scroll futur, horizon 1 an) + delta en jours sur l'accueil ; v1.4.2 = seuils d'alerte configurables (carte accueil + notification WorkManager 15 min + anti-spam) et backup JSON v2 avec paramètres ; v1.4.3 = fix du delta 6 h (#53) + régression n°4 (plateau EEn épinglé). Dette connue : fragments de
|
||
labs dans l'historique git (v1.1.0→v1.2.3) — cf §8.bis.*
|