From e3be3b36575b8a55770c03f3892a949fca8e01f1 Mon Sep 17 00:00:00 2001 From: Siphonight Date: Sat, 5 Sep 2026 22:14:19 +0200 Subject: [PATCH] =?UTF-8?q?v1.2.7=20:=20pr=C3=A9vision=20robuste=20aux=20o?= =?UTF-8?q?ublis/retards=20+=20rattrapage=20complet=20de=20la=20doc=20de?= =?UTF-8?q?=20dev?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - generateForecastDoses : les créneaux déjà passés (oubli d'injection) ne sont plus simulés — avant, dernier+intervalle tombait dans le passé → faux pic dans l'historique + rythme décalé ; la prévision démarre au premier créneau strictement futur, au rythme configuré - un retard décale toute la prévision (part de la dernière prise réelle) — comportement voulu, épinglé par 2 tests - doc §7.3b corrigée (mismatch doc-code détecté par l'utilisatrice) - docs : §13 écrasement, §19/§20 import, §14 #34/#35, §21, historique (lignes v1.2.2/6/7), footer — vérification intégrée dans le script de patch - versionCode 10, versionName 1.2.7 ; 64 tests verts --- app/build.gradle.kts | 4 +- .../hormonetrack/pk/PharmacokineticEngine.kt | 9 ++++ .../com/hormonetrack/pk/V120FeaturesTest.kt | 40 ++++++++++++++ docs/CHANGELOG.md | 16 +++++- docs/DEVELOPPEMENT.md | 54 ++++++++++++++----- 5 files changed, 108 insertions(+), 15 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 00ef6d2..9598dad 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "com.hormonetrack" minSdk = 26 targetSdk = 36 - versionCode = 9 - versionName = "1.2.6" + versionCode = 10 + versionName = "1.2.7" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/java/com/hormonetrack/pk/PharmacokineticEngine.kt b/app/src/main/java/com/hormonetrack/pk/PharmacokineticEngine.kt index b0b6834..e150926 100644 --- a/app/src/main/java/com/hormonetrack/pk/PharmacokineticEngine.kt +++ b/app/src/main/java/com/hormonetrack/pk/PharmacokineticEngine.kt @@ -319,6 +319,15 @@ object PharmacokineticEngine { val forecast = mutableListOf() var t = last.timestamp + intervalMs + // ⚠️ Oubli d'une injection : le premier créneau théorique tombe dans le + // PASSÉ (dernière prise + intervalle = jour de l'injection manquée) — + // on ne simule JAMAIS un créneau passé (faux pic dans l'historique) : + // on avance au premier créneau STRICTEMENT FUTUR, au rythme configuré. + // Un retard, lui, décale naturellement toute la prévision (elle part de + // la dernière prise réelle). + while (t <= nowMs) { + t += intervalMs + } while (t <= toMs) { forecast.add( DoseLog( diff --git a/app/src/test/java/com/hormonetrack/pk/V120FeaturesTest.kt b/app/src/test/java/com/hormonetrack/pk/V120FeaturesTest.kt index 193f686..3d25c23 100644 --- a/app/src/test/java/com/hormonetrack/pk/V120FeaturesTest.kt +++ b/app/src/test/java/com/hormonetrack/pk/V120FeaturesTest.kt @@ -50,6 +50,46 @@ class V120FeaturesTest { forecastIntervalDays = intervalDays ) + @Test + fun `forecast skips past slots after a missed dose (oubli)`() { + // Rythme 6 j, dernier rappel... dernière PRISE il y a 12 j (une injection + // a été manquée il y a 6 j) : le créneau théorique BASE-6j est passé. + val tr = eenTreatment(intervalDays = 6.0) + val doses = listOf( + DoseLog(treatmentId = 1, timestamp = BASE - 18 * DAY, doseAmount = 5.0), + DoseLog(treatmentId = 1, timestamp = BASE - 12 * DAY, doseAmount = 5.0) + ) + val forecast = PharmacokineticEngine.generateForecastDoses( + tr, doses, toMs = BASE + 30 * DAY, nowMs = BASE + ) + // Aucun créneau simulé dans le passé : premier = BASE+6 j (dernière+12 j + // est le premier créneau strictement futur au rythme de 6 j) + assertTrue(forecast.isNotEmpty()) + forecast.forEach { f -> + assertTrue("créneau passé simulé : ${f.timestamp}", f.timestamp > BASE) + } + assertEquals(BASE + 6 * DAY, forecast.first().timestamp) + forecast.zipWithNext { prev, cur -> + assertEquals(6 * DAY, cur.timestamp - prev.timestamp) + } + } + + @Test + fun `forecast follows a delayed last dose (retard d un jour)`() { + // Rythme 6 j, dernière prise faite avec 1 jour de retard (à J-5 au lieu + // de J-6) : TOUTE la prévision se décale d'un jour, en partant de la + // dernière prise réelle. + val tr = eenTreatment(intervalDays = 6.0) + val doses = listOf( + DoseLog(treatmentId = 1, timestamp = BASE - 5 * DAY, doseAmount = 5.0) + ) + val forecast = PharmacokineticEngine.generateForecastDoses( + tr, doses, toMs = BASE + 30 * DAY, nowMs = BASE + ) + assertEquals(BASE + 1 * DAY, forecast.first().timestamp) + assertTrue(forecast.all { it.timestamp > BASE }) + } + @Test fun `forecast doses follow the configured interval from the last real dose`() { val tr = eenTreatment(intervalDays = 7.0) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 8846c6d..e1609a2 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,7 +1,21 @@ # Changelog — HormoneTrack Format : [Keep a Changelog](https://keepachangelog.com/fr-FR/1.1.0/). -Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.2.6`, …). +Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.2.7`, …). + +## [1.2.7] — 2026-09-05 (versionCode 10) + +### Corrigé +- **Prévision cassée après un oubli d'injection** : le premier créneau projeté + tombait dans le PASSÉ (dernière prise + intervalle = jour de l'injection + manquée) → la courbe peignait un **faux pic dans l'historique** et tout le + rythme projeté était décalé. Désormais les créneaux déjà passés ne sont + jamais simulés : la prévision avance au premier créneau **strictement + futur** au rythme configuré. Un simple **retard**, lui, décale naturellement + toute la prévision (elle part de la dernière prise réelle — comportement + voulu, désormais testé). +- 2 nouveaux tests (64 au total) : oubli (créneaux passés sautés, espacement + conservé) et retard (décalage complet de la prévision). ## [1.2.6] — 2026-09-05 (versionCode 9) diff --git a/docs/DEVELOPPEMENT.md b/docs/DEVELOPPEMENT.md index 7faccf2..32472f5 100644 --- a/docs/DEVELOPPEMENT.md +++ b/docs/DEVELOPPEMENT.md @@ -69,6 +69,9 @@ Fonctionnalités v1 : | 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. | +| 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 @@ -256,14 +259,17 @@ Coupure par dose : `cutoffHours` = longueur de table (8001 h) pour les profils, dessine les deux modèles côte à côte depuis le même traitement (Bateman non concerné : les deux séries y sont identiques). - **`generateForecastDoses(treatment, doseLogs, toMs, nowMs)`** : projette les doses à - venir = dernière dose réelle + k × `forecastIntervalDays` jusqu'à `toMs`, strictement - après `nowMs` ; dose = standard du traitement, ester = override de la dernière - injection. Jamais persistées : uniquement passées à `computeCurve` par le ChartScreen - quand le chip « Prévision » est actif. + 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 » — renvoie des **copies** de traitements avec les scale factors - recalculés (médiane lab ÷ prédiction) + TConfig recalibré. Les valeurs stockées ne - bougent jamais ; HomeScreen et ChartScreen branchent dessus quand l'option est active. + 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) @@ -535,8 +541,15 @@ Pièges : - `BackupData{version=1, exportedAt, treatments[], doseLogs[], labResults[], tConfig}` → Gson - **Les IDs Room sont conservés** dans l'export et réinsérés tels quels → les FK dose→traitement restent valides -- Import = **ajout** (traitements → doses → labs) ; ré-import du même fichier → conflit - d'ID unique → exception catchée → `import_fail` (voulu ; un mode « replace » est en §20) +- 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 et les rappels sont reprogrammés + (`rescheduleAll(allTreatmentsOnce())`). 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{}` @@ -627,6 +640,19 @@ Historique complet — **à ne pas réintroduire** (utile pour diff/revert) : 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. +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()` + @@ -826,7 +852,7 @@ manquantes** (licences signées) — c'est comme ça que android-37 est arrivé. Volontaires (v1) : - Pas de ViewModel/DI (couplage UI↔repo via CompositionLocal) - Modèle T empirique (non publié) — étiqueté estimation partout -- Import JSON = ajout seulement (pas de mode replace/dédup) +- Import JSON = **écrasement** depuis v1.2.6 (mode fusion non implémenté) - `fallbackToDestructiveMigration()` — à retirer à la migration v2 du schéma - WorkManager déclaré non utilisé - Profils par **tables** (pas par formule) : les D/k1–k3 de l'ODS ne sont pas consommés — @@ -845,7 +871,7 @@ Volontaires (v1) : 1. **Robolectric + tests Compose** (VM Android en JVM — pas besoin d'appareil) 2. **Émulateur local** pour smoke-tests UI (sur demande, ~2–3 Go) 3. Mode « planifier les injections » (schedule récurrent → pré-remplir le log) -4. Import JSON : mode **replace** (wipe + insert) + détection de doublons +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 : **zoom** + tooltip au toucher (le pan est fait, v1.2.0) ; MaterialExpressiveTheme quand l'API passera publique (cf §3) @@ -888,9 +914,13 @@ Sur le téléphone de test (à compléter par l'utilisatrice) : - [ ] **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.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 --- -*Doc mise à jour le 5 sept. 2026 (v1.2.6) — build OK, 62/62 tests verts (44 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.* +*Doc mise à jour le 5 sept. 2026 (v1.2.7) — build OK, 62/62 tests verts (44 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.*