v1.2.7 : prévision robuste aux oublis/retards + rattrapage complet de la doc de dev
- 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
This commit is contained in:
parent
33e747de0c
commit
e3be3b3657
@ -12,8 +12,8 @@ android {
|
|||||||
applicationId = "com.hormonetrack"
|
applicationId = "com.hormonetrack"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 9
|
versionCode = 10
|
||||||
versionName = "1.2.6"
|
versionName = "1.2.7"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|||||||
@ -319,6 +319,15 @@ object PharmacokineticEngine {
|
|||||||
|
|
||||||
val forecast = mutableListOf<DoseLog>()
|
val forecast = mutableListOf<DoseLog>()
|
||||||
var t = last.timestamp + intervalMs
|
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) {
|
while (t <= toMs) {
|
||||||
forecast.add(
|
forecast.add(
|
||||||
DoseLog(
|
DoseLog(
|
||||||
|
|||||||
@ -50,6 +50,46 @@ class V120FeaturesTest {
|
|||||||
forecastIntervalDays = intervalDays
|
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
|
@Test
|
||||||
fun `forecast doses follow the configured interval from the last real dose`() {
|
fun `forecast doses follow the configured interval from the last real dose`() {
|
||||||
val tr = eenTreatment(intervalDays = 7.0)
|
val tr = eenTreatment(intervalDays = 7.0)
|
||||||
|
|||||||
@ -1,7 +1,21 @@
|
|||||||
# Changelog — HormoneTrack
|
# Changelog — HormoneTrack
|
||||||
|
|
||||||
Format : [Keep a Changelog](https://keepachangelog.com/fr-FR/1.1.0/).
|
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)
|
## [1.2.6] — 2026-09-05 (versionCode 9)
|
||||||
|
|
||||||
|
|||||||
@ -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 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 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.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
|
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
|
(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é :
|
dessine les deux modèles côte à côte depuis le même traitement (Bateman non concerné :
|
||||||
les deux séries y sont identiques).
|
les deux séries y sont identiques).
|
||||||
- **`generateForecastDoses(treatment, doseLogs, toMs, nowMs)`** : projette les doses à
|
- **`generateForecastDoses(treatment, doseLogs, toMs, nowMs)`** : projette les doses à
|
||||||
venir = dernière dose réelle + k × `forecastIntervalDays` jusqu'à `toMs`, strictement
|
venir = dernière dose réelle + k × `forecastIntervalDays`, **strictement après
|
||||||
après `nowMs` ; dose = standard du traitement, ester = override de la dernière
|
`nowMs`** (v1.2.7 : les créneaux déjà passés — cas d'un OUBLI — sont sautés au
|
||||||
injection. Jamais persistées : uniquement passées à `computeCurve` par le ChartScreen
|
rythme configuré, sinon faux pic dans l'historique ; un RETARD décale toute la
|
||||||
quand le chip « Prévision » est actif.
|
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
|
- **`autoCalibrated(treatments, doseLogs, labs, tConfig)`** : option « Calibration
|
||||||
automatique » — renvoie des **copies** de traitements avec les scale factors
|
automatique » (v1.2.1+) — renvoie `esterScales` (facteur **par ester**, cf §7.6)
|
||||||
recalculés (médiane lab ÷ prédiction) + TConfig recalibré. Les valeurs stockées ne
|
+ `tKPerEster` (k T **par ester**, cf §7.5) recalculés depuis les labs ;
|
||||||
bougent jamais ; HomeScreen et ChartScreen branchent dessus quand l'option est active.
|
`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)
|
### 7.4 Bateman (gel/patch/oral)
|
||||||
|
|
||||||
@ -535,8 +541,15 @@ Pièges :
|
|||||||
- `BackupData{version=1, exportedAt, treatments[], doseLogs[], labResults[], tConfig}` → Gson
|
- `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
|
- **Les IDs Room sont conservés** dans l'export et réinsérés tels quels → les FK
|
||||||
dose→traitement restent valides
|
dose→traitement restent valides
|
||||||
- Import = **ajout** (traitements → doses → labs) ; ré-import du même fichier → conflit
|
- Import : **mode ÉCRASEMENT (v1.2.6)** — les données actuelles sont effacées
|
||||||
d'ID unique → exception catchée → `import_fail` (voulu ; un mode « replace » est en §20)
|
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
|
- Transport : SAF (`CreateDocument("application/json")` / `OpenDocument`), écriture
|
||||||
`openOutputStream(uri, "wt")` ; ⚠️ pas de `return` dans un expression body `= try{}`
|
`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.
|
titre `[1.2.3]` : l'extraction cherchait `[vX.Y.Z]` → fallback silencieux.
|
||||||
→ `scripts/gitea-release.py` (corps = section CHANGELOG, APK attaché).
|
→ `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 —
|
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
|
la fenêtre AppCompat poussait le contenu sous la barre de statut ET les
|
||||||
TopAppBar M3 rajoutaient leur padding interne. → `enableEdgeToEdge()` +
|
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) :
|
Volontaires (v1) :
|
||||||
- Pas de ViewModel/DI (couplage UI↔repo via CompositionLocal)
|
- Pas de ViewModel/DI (couplage UI↔repo via CompositionLocal)
|
||||||
- Modèle T empirique (non publié) — étiqueté estimation partout
|
- 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
|
- `fallbackToDestructiveMigration()` — à retirer à la migration v2 du schéma
|
||||||
- WorkManager déclaré non utilisé
|
- WorkManager déclaré non utilisé
|
||||||
- Profils par **tables** (pas par formule) : les D/k1–k3 de l'ODS ne sont pas consommés —
|
- 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)
|
1. **Robolectric + tests Compose** (VM Android en JVM — pas besoin d'appareil)
|
||||||
2. **Émulateur local** pour smoke-tests UI (sur demande, ~2–3 Go)
|
2. **Émulateur local** pour smoke-tests UI (sur demande, ~2–3 Go)
|
||||||
3. Mode « planifier les injections » (schedule récurrent → pré-remplir le log)
|
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
|
5. Verrou biométrique (BiometricPrompt), widget, export CSV
|
||||||
6. Charts : **zoom** + tooltip au toucher (le pan est fait, v1.2.0) ;
|
6. Charts : **zoom** + tooltip au toucher (le pan est fait, v1.2.0) ;
|
||||||
MaterialExpressiveTheme quand l'API passera publique (cf §3)
|
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
|
- [ ] **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
|
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.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
|
- [ ] **v1.2.4** : passer un traitement à inactif → retiré de « Log rapide » et
|
||||||
du dropdown des nouvelles doses, rappel annulé, **mais sa simulation reste
|
du dropdown des nouvelles doses, rappel annulé, **mais sa simulation reste
|
||||||
sur le graphique** et la calibration couvre toujours ses périodes
|
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.*
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user