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"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 9
|
||||
versionName = "1.2.6"
|
||||
versionCode = 10
|
||||
versionName = "1.2.7"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
@ -319,6 +319,15 @@ object PharmacokineticEngine {
|
||||
|
||||
val forecast = mutableListOf<DoseLog>()
|
||||
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(
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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.*
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user