v1.7.1 : fix « une seule note sur deux » dans les Analyses (Android) + régression n°5
- LabsScreen : labNotesForDisplay (helper PUR, testé) — notes DISTINCTES d'une prise E2+T → DEUX lignes préfixées du marqueur (« E2 : … » / « T : … ») ; note identique (dialog commun) → une seule ligne (dédupliquée) ; vides ignorées ; note seule → brute (v1.0 préservé). L'ancien code ne prenait que la première note non vide (hypothèse « toutes identiques » cassée par l'édition unitaire) : l'autre note était conservée mais perdue à l'affichage. - RegressionUserCase5Test (7 tests) : NOUVEL export réel v1.7.0 (local-test-data/backup-v1.7.0.json, gitigné) — 3 traitements dont CPA oral, 64 doses, 28 labs ; la dernière prise porte EXACTEMENT le cas du bug (deux notes distinctes E2/T) : les deux affichées, aucune perdue ; plausibilité moteur (CPA oral + EEn actif + EV inactif simulé), prévision 7 j, notes de doses préservées. Aucune valeur de santé en dur (data-driven, garde §8.bis). - LabsGroupingTest : +5 tests sur labNotesForDisplay. 211 verts (181 sans données locales) + lint vert. - Validé émulateur §16.ter : APK release seedé avec l'export v1.7.0 → écran Analyses affiche « E2 : … » ET « T : … », 0 crash. - versionCode 34 / versionName 1.7.1. Web sync en miroir (web v1.7.1).
This commit is contained in:
parent
2c58418604
commit
5d81900a2d
@ -16,7 +16,7 @@ sauvegarde JSON. **100 % local, aucun compte, aucun serveur.**
|
||||
> à titre informatif — ce ne sont pas des mesures. Fie-toi toujours à tes analyses de
|
||||
> sang et aux consignes de ton endocrinologue.
|
||||
|
||||
- **Statut** : v1.7.0 — build Android ✅, **lint vert** ✅, **200 tests unitaires** ✅ (177 sans les données de test locales ; régressions épinglées sur données réelles **non versionnées**), intégration montre = notifications ✅, **dépôt Gitea privé + releases avec APK** ✅
|
||||
- **Statut** : v1.7.1 — build Android ✅, **lint vert** ✅, **211 tests unitaires** ✅ (181 sans les données de test locales ; régressions épinglées sur données réelles **non versionnées**), intégration montre = notifications ✅, **dépôt Gitea privé + releases avec APK** ✅
|
||||
- **Journal des versions** : [docs/CHANGELOG.md](docs/CHANGELOG.md)
|
||||
- **Guide utilisateur** : [docs/GUIDE_INSTALLATION.md](docs/GUIDE_INSTALLATION.md)
|
||||
- **Doc de développement** (architecture, maths, décisions, bugs) : [docs/DEVELOPPEMENT.md](docs/DEVELOPPEMENT.md)
|
||||
@ -110,7 +110,7 @@ par AGP si les licences sont signées). Le wrapper télécharge Gradle 9.7.1.
|
||||
git clone <repo> && cd HormoneTrack
|
||||
echo "sdk.dir=/chemin/vers/android-sdk" > local.properties # ou ANDROID_HOME
|
||||
./gradlew assembleDebug # APK : app/build/outputs/apk/debug/app-debug.apk
|
||||
./gradlew testDebugUnitTest # 200 tests (177 sans les données locales)
|
||||
./gradlew testDebugUnitTest # 211 tests (181 sans les données locales)
|
||||
./gradlew lint # lint vert obligatoire avant release
|
||||
```
|
||||
|
||||
|
||||
@ -17,8 +17,8 @@ android {
|
||||
// tag : v1.3.0→v1.3.2 contenaient tous versionCode 14 / "1.3.0"
|
||||
// (bump jamais commité) → BuildConfig.VERSION_NAME était faux dans
|
||||
// les APK publiés (Paramètres + titre du dialog « Nouveautés »).
|
||||
versionCode = 33
|
||||
versionName = "1.7.0"
|
||||
versionCode = 34
|
||||
versionName = "1.7.1"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
@ -62,6 +62,44 @@ fun groupLabsForDisplay(labs: List<LabResult>): List<Pair<Long, List<LabResult>>
|
||||
}
|
||||
.sortedByDescending { it.first }
|
||||
|
||||
/**
|
||||
* Notes à AFFICHER pour un groupe de labs (v1.7.1 — fix du bug « une seule
|
||||
* note sur deux s'affiche »).
|
||||
*
|
||||
* HISTORIQUE DU BUG : à l'origine, le dialog de création mettait LA MÊME note
|
||||
* aux deux entrées d'une prise E2+T — l'affichage ne prenait donc que la
|
||||
* première non vide (`firstNotNullOfOrNull`), hypothèse documentée « toutes
|
||||
* identiques en pratique ». Depuis l'édition UNITAIRE (tap → sélecteur E2/T),
|
||||
* chaque entrée porte SA note (cas réel : « Analyse faite suite à des maux de
|
||||
* tête. » sur E2 et « DHT : 12ng/dL » sur T) → l'autre note disparaissait de
|
||||
* l'affichage tout en restant bien sauvegardée. Même bug côté web (mirror
|
||||
* `.find()`), corrigé en miroir.
|
||||
*
|
||||
* RÈGLES :
|
||||
* - notes vides/blanches ignorées ;
|
||||
* - ordre du groupe préservé (E2 avant T — ordre de [groupLabsForDisplay]) ;
|
||||
* - note IDENTIQUE sur plusieurs entrées (création via le dialog commun) →
|
||||
* affichée UNE seule fois (pas de doublon) ;
|
||||
* - notes DISTINCTES → une ligne par note, préfixée du marqueur
|
||||
* (« E2 : … » / « T : … ») pour savoir à quelle entrée elle se rapporte ;
|
||||
* - une seule note distincte → affichée brute (comportement v1.0 préservé).
|
||||
*
|
||||
* PUR (aucune dépendance Android) → testé en JVM (LabsGroupingTest).
|
||||
*/
|
||||
fun labNotesForDisplay(group: List<LabResult>): List<String> {
|
||||
val notes = group.mapNotNull { lab ->
|
||||
lab.notes?.takeIf { it.isNotBlank() }
|
||||
?.let { lab.marker.uppercase() to it.trim() }
|
||||
}
|
||||
if (notes.isEmpty()) return emptyList()
|
||||
val distinct = notes.distinctBy { it.second } // même texte = une seule ligne
|
||||
return if (distinct.size == 1) {
|
||||
listOf(distinct.first().second)
|
||||
} else {
|
||||
distinct.map { (marker, note) -> "$marker : $note" }
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LabsScreen() {
|
||||
@ -114,9 +152,12 @@ fun LabsScreen() {
|
||||
supportingContent = {
|
||||
Column {
|
||||
Text(fmt.format(Instant.ofEpochMilli(ts)))
|
||||
// Notes du groupe (toutes identiques en pratique)
|
||||
group.firstNotNullOfOrNull { it.notes }?.takeIf { it.isNotBlank() }?.let {
|
||||
Text(it, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
// Notes du groupe (v1.7.1) : TOUTES les notes
|
||||
// distinctes, préfixées du marqueur quand elles
|
||||
// diffèrent — l'ancien code n'affichait que la
|
||||
// première non vide (bug « une note sur deux »)
|
||||
labNotesForDisplay(group).forEach { note ->
|
||||
Text(note, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -2,18 +2,27 @@ package com.hormonetrack.pk
|
||||
|
||||
import com.hormonetrack.data.model.LabResult
|
||||
import com.hormonetrack.ui.screens.groupLabsForDisplay
|
||||
import com.hormonetrack.ui.screens.labNotesForDisplay
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Regroupement de l'écran Analyses (v1.2.2) : une prise de sang (E2 + T au même
|
||||
* timestamp) s'affiche côte à côte ; E2 avant T ; tri chronologique décroissant.
|
||||
* v1.7.1 : affichage des NOTES du groupe ([labNotesForDisplay]) — fix du bug
|
||||
* « une seule note sur deux s'affiche » quand E2 et T portent des notes
|
||||
* DISTINCTES (chacune conservée en base, une seule affichée avant le fix).
|
||||
*/
|
||||
class LabsGroupingTest {
|
||||
|
||||
private fun lab(marker: String, ts: Long, value: Double) =
|
||||
LabResult(marker = marker, value = value, unit = if (marker == "E2") "pg/mL" else "ng/mL", timestamp = ts)
|
||||
|
||||
/** Lab avec note (les autres paramètres neutres). */
|
||||
private fun labWithNotes(marker: String, ts: Long, notes: String?) =
|
||||
LabResult(marker = marker, value = 1.0, unit = "pg/mL", timestamp = ts, notes = notes)
|
||||
|
||||
@Test
|
||||
fun `E2 and T at the same timestamp are grouped into one blood draw`() {
|
||||
val ts = 1_788_621_894_346L
|
||||
@ -53,4 +62,64 @@ class LabsGroupingTest {
|
||||
val groups = groupLabsForDisplay(labs)
|
||||
assertEquals(listOf("E2", "T", "PRL"), groups[0].second.map { it.marker.uppercase() })
|
||||
}
|
||||
|
||||
// ── Notes d'un groupe (v1.7.1 — fix « une seule note sur deux ») ────────
|
||||
|
||||
@Test
|
||||
fun `distinct notes on E2 and T are BOTH shown, prefixed by marker`() {
|
||||
// Cas réel remonté (paire v1.7.0 de l'utilisatrice) : note E2 ≠ note T
|
||||
val ts = 1_788_934_260_000L
|
||||
val group = listOf(
|
||||
labWithNotes("E2", ts, "Analyse faite suite à des maux de tête."),
|
||||
labWithNotes("T", ts, "DHT : 12 ng/dL"),
|
||||
)
|
||||
assertEquals(
|
||||
listOf(
|
||||
"E2 : Analyse faite suite à des maux de tête.",
|
||||
"T : DHT : 12 ng/dL",
|
||||
),
|
||||
labNotesForDisplay(group)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `identical notes on both entries are shown once (no duplicate)`() {
|
||||
// Création via le dialog commun : LA MÊME note part sur E2 et T
|
||||
val ts = 1_000L
|
||||
val group = listOf(
|
||||
labWithNotes("E2", ts, "Prise à jeun"),
|
||||
labWithNotes("T", ts, "Prise à jeun"),
|
||||
)
|
||||
assertEquals(listOf("Prise à jeun"), labNotesForDisplay(group))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `single note stays unprefixed (v1_0 behaviour preserved)`() {
|
||||
val ts = 2_000L
|
||||
val group = listOf(
|
||||
labWithNotes("E2", ts, "Seule note"),
|
||||
labWithNotes("T", ts, null),
|
||||
)
|
||||
assertEquals(listOf("Seule note"), labNotesForDisplay(group))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `blank and null notes are ignored, order follows the group (E2 before T)`() {
|
||||
val ts = 3_000L
|
||||
val group = listOf(
|
||||
labWithNotes("E2", ts, " "), // blanche → ignorée
|
||||
labWithNotes("T", ts, "note T"),
|
||||
labWithNotes("PRL", ts, "note PRL"),
|
||||
)
|
||||
// Ordre du groupe (déjà trié E2→T→PRL par le caller) : T avant PRL,
|
||||
// la note blanche d'E2 n'apparaît pas
|
||||
assertEquals(listOf("T : note T", "PRL : note PRL"), labNotesForDisplay(group))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `no notes at all yields no display lines`() {
|
||||
val ts = 4_000L
|
||||
val group = listOf(labWithNotes("E2", ts, null), labWithNotes("T", ts, ""))
|
||||
assertTrue(labNotesForDisplay(group).isEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
155
app/src/test/java/com/hormonetrack/pk/RegressionUserCase5Test.kt
Normal file
155
app/src/test/java/com/hormonetrack/pk/RegressionUserCase5Test.kt
Normal file
@ -0,0 +1,155 @@
|
||||
package com.hormonetrack.pk
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.hormonetrack.data.backup.BackupData
|
||||
import com.hormonetrack.data.model.LabResult
|
||||
import com.hormonetrack.data.model.TreatmentType
|
||||
import com.hormonetrack.ui.screens.labNotesForDisplay
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assume.assumeTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* **5ᵉ régression épinglée sur données réelles** (export v1.7.0, HORS dépôt
|
||||
* `local-test-data/backup-v1.7.0.json`, gitigné — données de santé).
|
||||
*
|
||||
* Contexte (remontée 2026-09-16) : dans une prise de sang E2+T, quand les
|
||||
* DEUX entrées portent des notes DISTINCTES (ex. une note clinique sur l'E2
|
||||
* et une note « DHT » sur la T), une seule s'affichait dans l'écran Analyses
|
||||
* — l'autre était bien conservée mais perdue à l'affichage. Fix :
|
||||
* `labNotesForDisplay` (Android + web en miroir), épinglé ici sur la VRAIE
|
||||
* paire de données. Profil mis à jour au passage : 3 traitements dont un CPA
|
||||
* oral (anti-androgène, Bateman) + EEn actif + EV historique INACTIF —
|
||||
* rappelle que l'inactif reste simulé (§6.bis).
|
||||
*
|
||||
* ⚠️ TOUTES les assertions sont DATA-DRIVEN (elles lisent le fichier local) :
|
||||
* aucune valeur de santé en dur dans ce fichier (garde de confidentialité
|
||||
* §8.bis — le scanner refuserait, et ce serait une fuite).
|
||||
*/
|
||||
class RegressionUserCase5Test {
|
||||
|
||||
companion object {
|
||||
private val DATA_FILE: File = listOf(
|
||||
File("../local-test-data/backup-v1.7.0.json"),
|
||||
File("local-test-data/backup-v1.7.0.json"),
|
||||
File("app/local-test-data/backup-v1.7.0.json")
|
||||
).firstOrNull { it.exists() } ?: File("../local-test-data/backup-v1.7.0.json")
|
||||
}
|
||||
|
||||
private lateinit var data: BackupData
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
assumeTrue(
|
||||
"local-test-data/backup-v1.7.0.json absent — tests ignorés (données locales)",
|
||||
DATA_FILE.exists()
|
||||
)
|
||||
if (!PKProfileStore.hasProfile("EV", "TFS")) {
|
||||
val file = listOf(
|
||||
File("src/main/assets/pk_profiles.json"),
|
||||
File("app/src/main/assets/pk_profiles.json")
|
||||
).first { it.exists() }
|
||||
PKProfileStore.initWithJson(file.readText())
|
||||
}
|
||||
data = Gson().fromJson(DATA_FILE.readText(), BackupData::class.java)
|
||||
}
|
||||
|
||||
/** Structure attendue de l'export (les nombres viennent du FICHIER). */
|
||||
@Test
|
||||
fun `backup parses with the expected shape`() {
|
||||
assertEquals(3, data.treatments.size)
|
||||
assertTrue("doses présentes", data.doseLogs.isNotEmpty())
|
||||
assertTrue("labs présents", data.labResults.isNotEmpty())
|
||||
// Profil : un anti-androgène ORAL actif, un E2 INACTIF resté dans les
|
||||
// données (§6.bis : jamais filtré de la simulation), un E2 actif
|
||||
assertEquals(1, data.treatments.count { it.type == TreatmentType.ANTI_ANDROGEN })
|
||||
assertEquals(1, data.treatments.count { !it.isActive })
|
||||
}
|
||||
|
||||
/**
|
||||
* LE fix de cette régression : la prise la plus récente porte DEUX notes
|
||||
* DISTINCTES (une par entrée) → [labNotesForDisplay] en rend DEUX,
|
||||
* préfixées du marqueur. Avant le fix, l'affichage n'en montrait qu'une
|
||||
* (première non vide) bien que les deux fussent conservées.
|
||||
*/
|
||||
@Test
|
||||
fun `the latest blood draw has two distinct notes and BOTH are displayed`() {
|
||||
val latestTs = data.labResults.maxOf { it.timestamp }
|
||||
val pair = data.labResults.filter { it.timestamp == latestTs }
|
||||
assertTrue("la dernière prise est une paire E2+T", pair.size >= 2)
|
||||
|
||||
val distinctNotes = pair.mapNotNull { it.notes?.takeIf { n -> n.isNotBlank() } }.distinct()
|
||||
assertTrue(
|
||||
"la dernière paire porte au moins deux notes distinctes (données du bug)",
|
||||
distinctNotes.size >= 2
|
||||
)
|
||||
val displayed = labNotesForDisplay(pair)
|
||||
assertEquals("DEUX lignes affichées", 2, displayed.size)
|
||||
assertTrue("chaque ligne est préfixée du marqueur", displayed.all { it.contains(" : ") })
|
||||
// Les DEUX textes d'origine se retrouvent (aucune perdue)
|
||||
for (note in distinctNotes) {
|
||||
assertTrue(displayed.any { it.endsWith(note) })
|
||||
}
|
||||
}
|
||||
|
||||
/** Même prise → note identique dupliquée = une seule ligne (pas de doublon). */
|
||||
@Test
|
||||
fun `a blood draw with the SAME note on both entries shows it once`() {
|
||||
val pair = listOf(
|
||||
LabResult(marker = "E2", value = 1.0, unit = "pg/mL", timestamp = 0, notes = "Prise à jeun"),
|
||||
LabResult(marker = "T", value = 1.0, unit = "ng/mL", timestamp = 0, notes = "Prise à jeun"),
|
||||
)
|
||||
assertEquals(1, labNotesForDisplay(pair).size)
|
||||
}
|
||||
|
||||
/** Notes de DOSES (côté injection) : parsées et préservées. */
|
||||
@Test
|
||||
fun `dose notes are preserved by the round-trip`() {
|
||||
assertTrue(
|
||||
"au moins une dose porte une note (données réelles)",
|
||||
data.doseLogs.any { !it.notes.isNullOrBlank() }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Plausibilité du moteur sur CE profil (CPA oral Bateman + EEn actif +
|
||||
* EV historique inactif) : niveau actuel physiologique, courbe 30 j non
|
||||
* vide et bornée, auto-calibration (activée dans les settings) couvre
|
||||
* l'ester actif sans planter.
|
||||
*/
|
||||
@Test
|
||||
fun `engine stays plausible on the new profile`() {
|
||||
val now = data.exportedAt + 300_000L // +5 min après l'export
|
||||
val level = PharmacokineticEngine.levelAt(
|
||||
data.treatments, data.doseLogs, now,
|
||||
tConfig = data.tConfig, scalePerEster = null, tKPerEster = null
|
||||
)
|
||||
assertTrue("E2 estimée physiologique (${level.e2})", level.e2 in 20.0..1000.0)
|
||||
assertTrue("T estimée physiologique (${level.t})", level.t in 0.01..3.0)
|
||||
|
||||
val curve = PharmacokineticEngine.computeCurve(
|
||||
data.treatments, data.doseLogs, now - 30 * 3_600_000L * 24, now,
|
||||
stepMs = 3_600_000L, tConfig = data.tConfig
|
||||
)
|
||||
assertTrue("courbe 30 j non vide", curve.isNotEmpty())
|
||||
|
||||
// Auto-calibration (option activée dans les settings de cet export)
|
||||
val auto = PharmacokineticEngine.autoCalibrated(
|
||||
data.treatments, data.doseLogs, data.labResults, data.tConfig
|
||||
)
|
||||
assertTrue("l'auto-calibration couvre au moins un ester", auto.calibratedEsters >= 1)
|
||||
}
|
||||
|
||||
/** Prévision : le traitement actif a une Posologie → créneaux projetés. */
|
||||
@Test
|
||||
fun `forecast slots exist for the active treatment`() {
|
||||
val active = data.treatments.first { it.isActive && it.type == TreatmentType.ESTRADIOL }
|
||||
val slots = PharmacokineticEngine.generateForecastDoses(
|
||||
active, data.doseLogs, data.exportedAt + 10L * 24 * 3_600_000L, data.exportedAt
|
||||
)
|
||||
assertTrue("créneaux prévisionnels générés (Posologie 7 j)", slots.isNotEmpty())
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,27 @@
|
||||
Format : [Keep a Changelog](https://keepachangelog.com/fr-FR/1.1.0/).
|
||||
Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.4.10`, …).
|
||||
|
||||
## [1.7.1] — 2026-09-16 (versionCode 34)
|
||||
|
||||
### Corrigé — une seule note sur deux s'affichait dans les Analyses
|
||||
|
||||
- **Bug remonté** (identique côté web) : dans une prise de sang E2+T, quand
|
||||
les deux entrées portent des notes **distinctes**, une seule s'affichait
|
||||
dans l'écran Analyses — l'autre était bien **conservée** (export/import
|
||||
l'avaient) mais perdue à l'affichage.
|
||||
- **Cause** : l'affichage ne prenait que la première note non vide du groupe
|
||||
(hypothèse historique « toutes identiques en pratique » — vraie à l'époque
|
||||
du dialog de création, fausse depuis l'édition unitaire E2/T).
|
||||
- **Fix** : `labNotesForDisplay` (helper pur, testé) — notes distinctes →
|
||||
une ligne par note **préfixée du marqueur** (« E2 : … » / « T : … ») ;
|
||||
note identique sur les deux entrées → une seule ligne (pas de doublon) ;
|
||||
notes vides ignorées ; une seule note → brute (comportement v1.0 gardé).
|
||||
- **Épinglé par** : 5 nouveaux tests LabsGroupingTest + **régression n°5
|
||||
data-driven sur le nouvel export réel** (v1.7.0 : la dernière prise
|
||||
porte exactement ce cas — deux notes distinctes E2/T, les deux
|
||||
affichées). Validé émulateur sur l'APK release seedé avec cet export.
|
||||
- **Web** : même fix en miroir (web v1.7.1, versions sync).
|
||||
|
||||
## [1.7.0] — 2026-09-13 (versionCode 33)
|
||||
|
||||
### Sauvegarde automatique quotidienne (opt-in)
|
||||
|
||||
@ -63,6 +63,7 @@ Fonctionnalités v1 :
|
||||
|
||||
| Date | Événement |
|
||||
|---|---|
|
||||
| 16 sept. 2026 (session v1.7.1) | **Fix « une seule note sur deux » dans les Analyses (Android + web)** : une prise E2+T avec des notes DISTINCTES (note clinique E2 + note « DHT » sur T — cas réel) n'en affichait qu'UNE (l'autre conservée mais perdue à l'affichage). Cause : affichage « première note non vide du groupe » — hypothèse historique « toutes identiques » (dialog commun) cassée par l'édition unitaire. FIX : `labNotesForDisplay` PUR des deux côtés (notes distinctes → préfixées du marqueur ; identiques → dédupliquées ; vides ignorées) + UI branchée. + **Nouvel export réel v1.7.0** (3 traitements dont CPA oral, 64 doses, 28 labs, notes doses ET labs) → `local-test-data/backup-v1.7.0.json` (gitigné) + **régression n°5** data-driven (7 tests : parsing, double-note épingle le fix, plausibilité moteur avec CPA, prévision 7 j). 11 nouveaux tests (211 verts / 181 sans données locales) + lint + check web. Validé émulateur §16.ter : seed v1.7.0 → écran Analyses affiche « E2 : … » ET « T : … » (dump), 0 crash. Web v1.7.1 sync (+ leçon : oubli d'entrée CHANGELOG = dialog « Nouveautés » vide, attrapé par l'E2E/§7). |
|
||||
| 13 sept. 2026 (session v1.7.0) | **Sauvegarde automatique quotidienne (opt-in) + unités des axes** : (a) Paramètres → « Sauvegarde automatique quotidienne » — dossier choisi UNE FOIS via SAF tree (ACTION_OPEN_DOCUMENT_TREE + takePersistableUriPermission, AUCUNE permission de stockage ; typiquement un dossier Owncloud synchronisé), WorkManager périodique 24 h (KEEP au démarrage, worker no-op si désactivé — pattern AlertWorker), **run immédiat à l'activation** (feedback + validation émulateur triviale), rétention configurable 1–30 copies (défaut 7) : fichier horodaté par run (`hormonetrack-auto-YYYYMMDD-HHmm.json`, jamais d'écrasement) + purge par la rétention PURE ([AutoBackupRetention] — ne touche JAMAIS exports manuels/logs/étrangers, keep clampé ≥ 1) ; statut « dernier run » persisté + AppLog complet ; contenu = backup v2 COMPLET (importable tel quel). (b) **Unités des axes du graphique** (remontée « jamais ajoutées depuis v1.0 ») : pg/mL (E2, gauche) / ng/mL (T, droite) au sommet des colonnes de labels (padTop 12→26 dp), portées aussi côté web (versions sync). 9 nouveaux tests (200 verts / 177 sans données locales) + lint vert ; proguard : `-keep` AutoBackupWorker (réflexion WorkManager — leçon #64). Validé émulateur §16.ter APK release (dossier Download via le picker SAF piloté uiautomator, fichier écrit, rétention, unités visibles, 0 crash). |
|
||||
| 12 sept. 2026 (session v1.6.0) | **« Tracé labs » PROLONGÉ au-delà du dernier lab** (demande) : le tracé s'arrêtait AU dernier lab (v1.5.0) — justement la période la plus récente était invisible. FIX : chip `Prolonger` (5ᵉ de la rangée, off par défaut, désactivé tant que Tracé labs est off) → au-delà du dernier lab significatif, `courbe(t) = M(t) × ρ_last` (ρ CONSTANT — pas d'extrapolation de pente, elle divergerait sans base physiologique) ; M(t) inclut AUTOMATIQUEMENT les doses loguées après le dernier lab (une injection EV après des labs EEn refait monter la courbe — épinglé par test) ; horizon = dernière dose E2 + cutoffHours de son traitement (une seule source de vérité : `cutoffHours` rendu public) ; série SPLITTÉE en "LAB" (ancré) / "LABX" (prolongé, rose atténué α 0,55 + légende dédiée — l'estimation ne se confond pas avec le mesuré) ; branche "LABX" EXPLICITE dans le when de légende AVANT le else (leçon #63) ; **AVERTISSEMENT visible sous la légende quand la prolongation est affichée** (demande) : « simple simulation, sans garantie de correspondre au réel, basée sur tes labs qui peuvent eux-mêmes être erronés » (string FR/EN `lab_track_extend_warning`). < 2 ancres, modèle déjà éteint au dernier lab, ou demande finissant avant → pas de prolongation (retour v1.5.0 exact, épinglé bit-compatible). 8 nouveaux tests (191 verts / 168 sans données locales) + lint vert. **+ Rattrapage validation émulateur §16.ter (oubliée, demandée par l'utilisatrice) → bug #64 TROUVÉ** : « Tracé labs » vide EN RELEASE SEULE (R8 full mode avait REMOVÉ la classe — mapping `R8$$REMOVED$$CLASS` — et inliné le calcul dans le producer ; debug OK, 191 tests JVM aveugles). FIX : `-keep class ...LabTrajectoryModel { *; }` ; validé émulateur APK release re-buildé avec les données réelles seedées : ancrée (7090 px rose + légende), prolongée (légende + avertissement), 0 crash. Tag v1.6.0 reposé sur le commit de fix AVANT publication d'APK (checklist §16 étape 4). **Checklist §16 : étape 3.bis « validation émulateur release OBLIGATOIRE avant tag » ajoutée.** |
|
||||
| 11 sept. 2026 (session v1.5.0) | **« Tracé labs »** (demande débattue) : courbe hybride `M(t)×ρ(t)` ancrée sur les labs — ρ log-linéaire entre labs, garde #61, E2 seul, fenêtre labs, chip off par défaut + scroll horizontal CONFINÉ (chip 4ᵉ coupé hors fenêtre, constat émulateur) + leçon #63 légende dupliquée ("LAB" tombant dans else TFS) ; ChartSeries.showExtrema=false pour la série ; 11 tests LabTrajectoryModelTest (183 verts) + lint ; validé émulateur (courbe passe exactement sur le lab 248, 0 crash). |
|
||||
@ -596,9 +597,9 @@ constant à droite comme l'interpolation aboutit à ρ_last à gauche).
|
||||
|
||||
## 8. Tests unitaires
|
||||
|
||||
**200 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — **177 sans
|
||||
les données de test locales** (cf §8.bis : les 4 classes de régression,
|
||||
6/6/6/5 tests, sont skippées via `Assume`). Dépendance : JUnit 4.13.2.
|
||||
**211 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — **181 sans
|
||||
les données de test locales** (cf §8.bis : les 5 classes de régression,
|
||||
6/6/6/5/7 tests, 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/…`).
|
||||
|
||||
@ -653,11 +654,14 @@ 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,
|
||||
`backup-v1.3.1.json`, `backup-v1.4.2.json` et `backup-v1.7.0.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) ;
|
||||
les settings v2), `RegressionUserCase5Test` lit **v1.7.0** (profil à 3
|
||||
traitements dont CPA oral ; paire E2+T avec DEUX notes distinctes —
|
||||
épine le fix « une note sur deux » v1.7.1) ;
|
||||
- `.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 ;
|
||||
@ -671,8 +675,8 @@ dépôt (ni maintenant, ni si le repo devient public un jour).
|
||||
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 177 tests au lieu de 200 (v1.7.0 ; chiffres historiques : 44/87 à v1.3.x,
|
||||
150/172 à v1.4.x, 160/183 à v1.5.0, 168/191 à v1.6.0) ;
|
||||
exécute 181 tests au lieu de 211 (v1.7.1 ; chiffres historiques : 44/87 à v1.3.x,
|
||||
150/172 à v1.4.x, 160/183 à v1.5.0, 168/191 à v1.6.0, 177/200 à v1.7.0) ;
|
||||
- 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
|
||||
@ -696,9 +700,13 @@ dépôt (ni maintenant, ni si le repo devient public un jour).
|
||||
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
|
||||
- **`LabsGroupingTest`** (9) : 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).
|
||||
ordre chronologique décroissant). v1.7.1 — **notes d'un groupe**
|
||||
(`labNotesForDisplay`) : notes distinctes E2/T → DEUX lignes préfixées du
|
||||
marqueur (fix « une seule note sur deux »), note identique → une seule
|
||||
ligne (dédupliquée), note seule → brute (v1.0 préservé), vides/blanches
|
||||
ignorées, aucune note → aucune ligne.
|
||||
- **`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
|
||||
@ -770,6 +778,14 @@ dépôt (ni maintenant, ni si le repo devient public un jour).
|
||||
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.**
|
||||
- **`RegressionUserCase5Test`** (7) : v1.7.1 — 5ᵉ régression épinglée sur
|
||||
les données réelles (export v1.7.0, HORS dépôt) : profil à 3 traitements
|
||||
(CPA oral Bateman + EEn actif + EV inactif simulé, §6.bis), **le fix
|
||||
« une note sur deux » épinglé sur la VRAIE paire** (dernière prise E2+T
|
||||
avec deux notes distinctes → `labNotesForDisplay` en rend deux, préfixées ;
|
||||
note identique → une seule), notes de DOSES préservées, plausibilité
|
||||
moteur (niveau physiologique, courbe 30 j, auto-calibration couvre un
|
||||
ester), prévision 7 j. **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,
|
||||
@ -2044,6 +2060,11 @@ Sur le téléphone de test (à compléter par l'utilisatrice) :
|
||||
gauche (E2) et « ng/mL » au sommet de l'axe droit (T — disparaît
|
||||
avec le toggle T), au-dessus des labels numériques ; portées web
|
||||
(versions sync)
|
||||
- [ ] **v1.7.1** : **notes des analyses** : une prise E2+T avec des notes
|
||||
DISTINCTES affiche LES DEUX lignes (« E2 : … » / « T : … ») ;
|
||||
note identique sur les deux → une seule ligne ; la note seule reste
|
||||
brute ; ré-éditer une entrée → sa note se met à jour sans effacer
|
||||
l'autre
|
||||
- [ ] **v1.4.10** : **pan sur la vue 24 h** : glisser horizontalement à
|
||||
24 h doit déplacer la courbe (fix #62 : les deltas < 1 h s'accumulent
|
||||
— avant, le pan ne bougeait jamais sur 24 h) ; tester aussi à 7 j et
|
||||
@ -2064,7 +2085,7 @@ Sur le téléphone de test (à compléter par l'utilisatrice) :
|
||||
tap ; et TOUTES les courbes affichées reflètent la calibration (test)
|
||||
|
||||
---
|
||||
*Doc mise à jour le 13 sept. 2026 (v1.7.0) — build OK, lint vert, 200/200 tests verts (177 sans
|
||||
*Doc mise à jour le 16 sept. 2026 (v1.7.1) — build OK, lint vert, 211/211 tests verts (181 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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user