v1.11.0 : inactifs regroupés en bas de la page Traitements + cartes atténuées

- Demande : « les traitements inactifs sont peu discernables — les placer
  à la fin (tout en bas), et peut-être les rendre plus distincts ».
- Helper pur treatmentsForDisplay (tri STABLE : actifs d'abord, inactifs
  après, ordre relatif conservé dans chaque groupe).
- LazyColumn en 3 sections : actifs / en-tête inactive_section (seulement
  si des inactifs existent) / inactifs — cartes alpha 0.55 en plus du
  badge, lisibles et tappables (l'édition d'un archivé reste possible).
- Miroir web : treatmentsForDisplay dans js/data/models.js + rendu
  sections + opacity 0.55 + i18n inactive_section FR/EN (web v1.11.0 sync).
- 5 tests JVM TreatmentsDisplayTest + 4 tests web (miroir).

264 tests JVM + 14 UI + lint verts ; web 184 tests + E2E verts (check.sh).
Validé émulateur sur APK RELEASE avec données réelles seedées (5 actifs en
haut, en-tête + EV-old/EEn-old tout en bas, 0 crash).
This commit is contained in:
Siphonight 2026-09-29 07:31:25 +02:00
parent 5cd1023af1
commit a0f3ac59f9
9 changed files with 188 additions and 17 deletions

View File

@ -18,8 +18,8 @@ or Huawei Health) and JSON backup. **100% local, no account, no server.**
> for informational purposes — they are not measurements. Always rely on your > for informational purposes — they are not measurements. Always rely on your
> blood tests and on your endocrinologist's guidance. > blood tests and on your endocrinologist's guidance.
- **Status**: v1.10.0 — Android build ✅, **lint clean** ✅, **259 unit tests** ✅ - **Status**: v1.11.0 — Android build ✅, **lint clean** ✅, **264 unit tests** ✅
(225 without the local test data; 6 regressions pinned on real data (230 without the local test data; 6 regressions pinned on real data
**not committed to the repo**), **14 Compose UI tests** ✅ (emulator), **not committed to the repo**), **14 Compose UI tests** ✅ (emulator),
smartwatch integration = notifications ✅, **private Gitea repo + releases smartwatch integration = notifications ✅, **private Gitea repo + releases
with APK** ✅ with APK** ✅
@ -70,7 +70,8 @@ or Huawei Health) and JSON backup. **100% local, no account, no server.**
**interval in days between doses** displayed **interval in days between doses** displayed
- **Inactive treatments**: an archived treatment disappears from input and - **Inactive treatments**: an archived treatment disappears from input and
reminders, but **its whole history stays simulated and calibrated** — useful reminders, but **its whole history stays simulated and calibrated** — useful
for a valerate → enanthate transition for a valerate → enanthate transition; **grouped at the very bottom** of the
Treatments page under a dedicated header, dimmed cards (v1.11.0)
- **Blood tests**: **E2 + T in a single entry** (each optional), displayed - **Blood tests**: **E2 + T in a single entry** (each optional), displayed
**side by side** when they share the same date/time, editable (tap → pick the **side by side** when they share the same date/time, editable (tap → pick the
entry); T units: ng/mL, ng/dL, ng/L, nmol/L entry); T units: ng/mL, ng/dL, ng/L, nmol/L
@ -129,7 +130,7 @@ Gradle 9.7.1.
git clone <repo> && cd HormoneTrack git clone <repo> && cd HormoneTrack
echo "sdk.dir=/path/to/android-sdk" > local.properties # or ANDROID_HOME echo "sdk.dir=/path/to/android-sdk" > local.properties # or ANDROID_HOME
./gradlew assembleDebug # APK: app/build/outputs/apk/debug/app-debug.apk ./gradlew assembleDebug # APK: app/build/outputs/apk/debug/app-debug.apk
./gradlew testDebugUnitTest # 259 tests (225 without local test data) ./gradlew testDebugUnitTest # 264 tests (230 without local test data)
./gradlew connectedDebugAndroidTest # 14 UI tests (emulator/device required) ./gradlew connectedDebugAndroidTest # 14 UI tests (emulator/device required)
./gradlew lint # clean lint required before a release ./gradlew lint # clean lint required before a release
``` ```

View File

@ -18,7 +18,7 @@ sauvegarde JSON. **100 % local, aucun compte, aucun serveur.**
> à titre informatif — ce ne sont pas des mesures. Fie-toi toujours à tes analyses de > à titre informatif — ce ne sont pas des mesures. Fie-toi toujours à tes analyses de
> sang et aux consignes de ton endocrinologue. > sang et aux consignes de ton endocrinologue.
- **Statut** : v1.10.0 — build Android ✅, **lint vert** ✅, **259 tests unitaires** ✅ (225 sans les données de test locales ; 6 régressions épinglées sur données réelles **non versionnées**), **14 tests UI Compose** ✅ (émulateur), intégration montre = notifications ✅, **dépôt Gitea privé + releases avec APK** ✅ - **Statut** : v1.11.0 — build Android ✅, **lint vert** ✅, **264 tests unitaires** ✅ (230 sans les données de test locales ; 6 régressions épinglées sur données réelles **non versionnées**), **14 tests UI Compose** ✅ (émulateur), intégration montre = notifications ✅, **dépôt Gitea privé + releases avec APK** ✅
- **Journal des versions** : [docs/CHANGELOG.md](docs/CHANGELOG.md) - **Journal des versions** : [docs/CHANGELOG.md](docs/CHANGELOG.md)
- **Guide utilisateur** : [docs/GUIDE_INSTALLATION.md](docs/GUIDE_INSTALLATION.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) - **Doc de développement** (architecture, maths, décisions, bugs) : [docs/DEVELOPPEMENT.md](docs/DEVELOPPEMENT.md)
@ -70,7 +70,8 @@ sauvegarde JSON. **100 % local, aucun compte, aucun serveur.**
**intervalle en jours entre dosages** affiché **intervalle en jours entre dosages** affiché
- **Traitements inactifs** : un traitement archivé n'apparaît plus dans la saisie - **Traitements inactifs** : un traitement archivé n'apparaît plus dans la saisie
ni dans les rappels, mais **tout son historique reste simulé et calibré** — ni dans les rappels, mais **tout son historique reste simulé et calibré** —
utile pour une transition valerate → enanthate utile pour une transition valerate → enanthate ; **regroupés tout en bas** de la
page Traitements sous un en-tête dédié, cartes atténuées (v1.11.0)
- **Analyses de sang** : **E2 + T en une seule entrée** (chacune optionnelle), affichées - **Analyses de sang** : **E2 + T en une seule entrée** (chacune optionnelle), affichées
**côte à côte** quand elles partagent la même date/heure, éditables (tap → choix de **côte à côte** quand elles partagent la même date/heure, éditables (tap → choix de
l'entrée) ; unités T : ng/mL, ng/dL, ng/L, nmol/L l'entrée) ; unités T : ng/mL, ng/dL, ng/L, nmol/L
@ -128,7 +129,7 @@ par AGP si les licences sont signées). Le wrapper télécharge Gradle 9.7.1.
git clone <repo> && cd HormoneTrack git clone <repo> && cd HormoneTrack
echo "sdk.dir=/chemin/vers/android-sdk" > local.properties # ou ANDROID_HOME echo "sdk.dir=/chemin/vers/android-sdk" > local.properties # ou ANDROID_HOME
./gradlew assembleDebug # APK : app/build/outputs/apk/debug/app-debug.apk ./gradlew assembleDebug # APK : app/build/outputs/apk/debug/app-debug.apk
./gradlew testDebugUnitTest # 259 tests (225 sans les données locales) ./gradlew testDebugUnitTest # 264 tests (230 sans les données locales)
./gradlew connectedDebugAndroidTest # 14 tests UI (émulateur/appareil requis) ./gradlew connectedDebugAndroidTest # 14 tests UI (émulateur/appareil requis)
./gradlew lint # lint vert obligatoire avant release ./gradlew lint # lint vert obligatoire avant release
``` ```

View File

@ -33,8 +33,8 @@ android {
// tag : v1.3.0→v1.3.2 contenaient tous versionCode 14 / "1.3.0" // tag : v1.3.0→v1.3.2 contenaient tous versionCode 14 / "1.3.0"
// (bump jamais commité) → BuildConfig.VERSION_NAME était faux dans // (bump jamais commité) → BuildConfig.VERSION_NAME était faux dans
// les APK publiés (Paramètres + titre du dialog « Nouveautés »). // les APK publiés (Paramètres + titre du dialog « Nouveautés »).
versionCode = 47 versionCode = 48
versionName = "1.10.1" versionName = "1.11.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {

View File

@ -27,6 +27,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.hormonetrack.R import com.hormonetrack.R
@ -36,6 +37,21 @@ import com.hormonetrack.data.model.modelLabelRes
import com.hormonetrack.ui.LocalAppContainer import com.hormonetrack.ui.LocalAppContainer
import com.hormonetrack.ui.components.formatDose import com.hormonetrack.ui.components.formatDose
/**
* ORDRE D'AFFICHAGE de la page Traitements (v1.11.0, demande utilisatrice) :
* les ACTIFS d'abord, les INACTIFS regroupés TOUT EN BAS — l'ancien ordre
* brut du DAO mélangeait les deux groupes et le badge discret ne suffisait
* plus à les discerner (« peu discernables », remontée).
*
* Tri STABLE : l'ordre relatif À L'INTÉRIEUR de chaque groupe est conservé
* (le tri ne réordonne jamais les actifs entre eux, ni les inactifs entre
* eux). PUR (aucune dépendance Android) → testable en JVM
* (TreatmentsDisplayTest) ; miroir web `treatmentsForDisplay`
* (js/data/models.js, mêmes sémantiques).
*/
fun treatmentsForDisplay(treatments: List<Treatment>): List<Treatment> =
treatments.sortedBy { !it.isActive }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun TreatmentsScreen( fun TreatmentsScreen(
@ -45,6 +61,11 @@ fun TreatmentsScreen(
val container = LocalAppContainer.current val container = LocalAppContainer.current
val repo = container.repository val repo = container.repository
val treatments by repo.allTreatments.collectAsState(initial = emptyList()) val treatments by repo.allTreatments.collectAsState(initial = emptyList())
// v1.11.0 : actifs d'abord, inactifs regroupés tout en bas
// (cf [treatmentsForDisplay]) — mémoïsé sur la liste brute.
val displayTreatments = remember(treatments) { treatmentsForDisplay(treatments) }
val activeTreatments = displayTreatments.filter { it.isActive }
val inactiveTreatments = displayTreatments.filter { !it.isActive }
Scaffold( Scaffold(
floatingActionButton = { floatingActionButton = {
@ -58,22 +79,44 @@ fun TreatmentsScreen(
title = { Text(stringResource(R.string.nav_treatments)) } title = { Text(stringResource(R.string.nav_treatments)) }
) )
LazyColumn(Modifier.fillMaxSize()) { LazyColumn(Modifier.fillMaxSize()) {
items(treatments, key = { it.id }) { tr -> items(activeTreatments, key = { it.id }) { tr ->
TreatmentCard(tr, onClick = { onEdit(tr.id) }) TreatmentCard(tr, onClick = { onEdit(tr.id) })
} }
// En-tête de section SEULEMENT s'il existe des inactifs —
// la séparation visuelle (en-tête + atténuation, cf
// TreatmentCard) rend le groupe lisible d'un coup d'œil.
if (inactiveTreatments.isNotEmpty()) {
item(key = "inactive_section") {
Text(
stringResource(R.string.inactive_section),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp)
)
}
}
items(inactiveTreatments, key = { it.id }) { tr ->
TreatmentCard(tr, dimmed = true, onClick = { onEdit(tr.id) })
}
} }
} }
} }
} }
/**
* Carte d'un traitement. @param dimmed : carte INACTIVE (v1.11.0) → contenu
* atténué (alpha réduit) en PLUS du badge : second signal visuel demandé
* (« plus distincts des actifs ») — la carte reste lisible et tappable.
*/
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun TreatmentCard(tr: Treatment, onClick: () -> Unit) { private fun TreatmentCard(tr: Treatment, dimmed: Boolean = false, onClick: () -> Unit) {
Card( Card(
onClick = onClick, onClick = onClick,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 6.dp) .padding(horizontal = 16.dp, vertical = 6.dp)
.alpha(if (dimmed) 0.55f else 1f)
) { ) {
Column(Modifier.padding(12.dp)) { Column(Modifier.padding(12.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {

View File

@ -144,6 +144,9 @@
<string name="dose_unit">Unité</string> <string name="dose_unit">Unité</string>
<string name="active">Actif</string> <string name="active">Actif</string>
<string name="inactive">inactif</string> <string name="inactive">inactif</string>
<!-- v1.11.0 : en-tête de la section des traitements inactifs (regroupés
en bas de la page Traitements) -->
<string name="inactive_section">Traitements inactifs</string>
<string name="no_treatment_hint">Crée d\'abord un traitement (onglet Traitements).</string> <string name="no_treatment_hint">Crée d\'abord un traitement (onglet Traitements).</string>
<string name="type_estradiol">Œstradiol</string> <string name="type_estradiol">Œstradiol</string>

View File

@ -144,6 +144,9 @@
<string name="dose_unit">Unit</string> <string name="dose_unit">Unit</string>
<string name="active">Active</string> <string name="active">Active</string>
<string name="inactive">inactive</string> <string name="inactive">inactive</string>
<!-- v1.11.0 : en-tête de la section des traitements inactifs (regroupés
en bas de la page Traitements) -->
<string name="inactive_section">Inactive treatments</string>
<string name="no_treatment_hint">Create a treatment first (Treatments tab).</string> <string name="no_treatment_hint">Create a treatment first (Treatments tab).</string>
<string name="type_estradiol">Estradiol</string> <string name="type_estradiol">Estradiol</string>

View File

@ -0,0 +1,86 @@
package com.hormonetrack.ui
import com.hormonetrack.data.model.AdministrationRoute
import com.hormonetrack.data.model.Treatment
import com.hormonetrack.data.model.TreatmentType
import com.hormonetrack.ui.screens.treatmentsForDisplay
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Tests de l'ORDRE D'AFFICHAGE de la page Traitements (v1.11.0, demande
* utilisatrice : « les traitements inactifs faudrait les placer à la fin
* (tout en bas) de la liste ») — [treatmentsForDisplay] :
* - les ACTIFS d'abord, les INACTIFS regroupés après ;
* - tri STABLE : l'ordre relatif DANS chaque groupe est conservé (le tri
* ne réordonne jamais les actifs entre eux, ni les inactifs entre eux) ;
* - cas limites : liste vide, tout actif, tout inactif (identité).
*
* PUR (aucune dépendance Android) ; miroir web `treatmentsForDisplay`
* (js/data/models.js, tests/treatments-display.test.js).
*/
class TreatmentsDisplayTest {
private fun tr(id: Long, name: String, isActive: Boolean) = Treatment(
id = id, name = name, type = TreatmentType.ESTRADIOL,
route = AdministrationRoute.INJECTION_IM,
doseAmount = 5.0, doseUnit = "mg",
isActive = isActive, createdAt = 0
)
@Test
fun `inactive treatments are grouped at the end`() {
val mixed = listOf(
tr(1, "EEn", true),
tr(2, "EV-old", false),
tr(3, "CPA", true),
tr(4, "EEn-old", false),
tr(5, "Ritaline", true)
)
val display = treatmentsForDisplay(mixed)
assertEquals(
"actifs d'abord (ordre conservé), inactifs après (ordre conservé)",
listOf(1L, 3L, 5L, 2L, 4L),
display.map { it.id }
)
assertEquals(
"aucun inactif avant un actif",
0,
display.indexOfFirst { !it.isActive }.let { firstInactive ->
if (firstInactive < 0) 0
else display.subList(firstInactive, display.size).count { it.isActive }
}
)
}
@Test
fun `ordering inside each group is preserved (stable sort)`() {
val mixed = listOf(
tr(10, "A-inactif", false),
tr(11, "B-actif", true),
tr(12, "C-inactif", false),
tr(13, "D-actif", true)
)
assertEquals(
listOf(11L, 13L, 10L, 12L),
treatmentsForDisplay(mixed).map { it.id }
)
}
@Test
fun `all active - order unchanged`() {
val all = listOf(tr(3, "C", true), tr(1, "A", true), tr(2, "B", true))
assertEquals(listOf(3L, 1L, 2L), treatmentsForDisplay(all).map { it.id })
}
@Test
fun `all inactive - order unchanged`() {
val all = listOf(tr(2, "B", false), tr(1, "A", false))
assertEquals(listOf(2L, 1L), treatmentsForDisplay(all).map { it.id })
}
@Test
fun `empty list stays empty`() {
assertEquals(0, treatmentsForDisplay(emptyList()).size)
}
}

View File

@ -3,6 +3,31 @@
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.4.10`, …). Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.4.10`, …).
## [1.11.0] — 2026-09-29 (versionCode 48)
### Ajouté — les traitements inactifs regroupés en bas de la page Traitements
- **Demande** : « les traitements inactifs sont peu discernables par rapport
aux traitements actifs — les placer à la fin (tout en bas) de la liste
pour éviter les confusions ».
- **Tri** : les ACTIFS d'abord, les INACTIFS regroupés **tout en bas**, sous
un en-tête de section « Traitements inactifs » (n'apparaît que s'il
existe des inactifs). Tri STABLE : l'ordre relatif à l'intérieur de
chaque groupe est conservé — le tri ne réordonne jamais les actifs entre
eux, ni les inactifs entre eux.
- **Distinction visuelle renforcée** : les cartes inactives sont ATTÉNUÉES
(alpha 0,55) en plus du badge « inactif » — second signal demandé
(« peut-être les rendre plus distincts ») ; elles restent lisibles et
tappables (l'édition d'un traitement archivé doit rester possible).
- Helper pur `treatmentsForDisplay` (Kotlin + **miroir web** en
`js/data/models.js`) — 5 tests JVM (`TreatmentsDisplayTest`) + 4 tests
web (`tests/treatments-display.test.js`).
- Vérifié émulateur avec données réelles seedées : les 5 actifs en haut,
en-tête puis les 2 inactifs (EV-old, EEn-old) tout en bas.
Vérifié : **264 tests JVM** (230 sans données locales) + **14 tests UI** +
lint verts ; **web 184 tests + E2E verts** (`check.sh`).
## [1.10.1] — 2026-09-28 (versionCode 47) ## [1.10.1] — 2026-09-28 (versionCode 47)
### Amélioré — durcissement du système de rappels (revue de code post-v1.10.0) ### Amélioré — durcissement du système de rappels (revue de code post-v1.10.0)

View File

@ -63,6 +63,7 @@ Fonctionnalités v1 :
| Date | Événement | | Date | Événement |
|---|---| |---|---|
| 29 sept. 2026 (session v1.11.0) | **Page Traitements : inactifs regroupés en bas + atténués** (demande : « peu discernables — les placer à la fin, et peut-être les rendre plus distincts »). Helper PUR `treatmentsForDisplay` (tri STABLE : actifs d'abord, inactifs après, ordre relatif conservé DANS chaque groupe) + LazyColumn en 3 sections (actifs / en-tête `inactive_section` SEULEMENT si des inactifs existent / inactifs `dimmed = true`) + cartes inactives **alpha 0,55** en plus du badge (lisibles et tappables — l'édition d'un archivé doit rester possible). **Miroir web** : `treatmentsForDisplay` dans `js/data/models.js` (node-safe, pas de DOM) + rendu sections + `opacity:0.55` + clés i18n `inactive_section` FR/EN. Tests : **5 JVM** (`TreatmentsDisplayTest` : regroupement, stabilité, tout-actif, tout-inactif, vide) + **4 web** (`tests/treatments-display.test.js`) — pas de test UI instrumenté pour l'en-tête (dépendrait de données inactives, contre la convention data-indépendance §8.ter) : couverture JVM + validation émulateur (5 actifs en haut, en-tête + EV-old/EEn-old tout en bas, dumps tr1/tr2). 264 tests JVM + lint ; web 184 tests + E2E verts (check.sh). |
| 28 sept. 2026 (session v1.10.1) | **Durcissement rappels — revue de code post-v1.10.0** (audit demandé : « optimisations ou problèmes, web et Android »). (a) **`launchMode="singleTop"`** : le combo `standard`+`CLEAR_TOP` recréait l'activité au tap notification app-ouverte et rendait le `onNewIntent` de la v1.10.0 inopérant (code mort) — désormais délivré SANS recreation (état UI préservé). (b) **`EXTRA_PLANNED_AT`** : le garde « créneau déjà honoré » évaluait le jour au moment du FEU — une alarme inexacte (`setWindow`/Doze) glissant 23:58→00:02 évaluait le mauvais jour ; l'instant planifié voyage dans l'intent (scheduleFor), le snooze le FORWARDE, le garde évalue le jour du CRÉNEAU ; vérifié émulateur (broadcast `planned_at` hier → notif sautée). (c) **`BootReceiver` consolidé** (dette §20.bis #2 FAIT) : `goAsync`+coroutine, `runBlocking` supprimés. (d) Style (import `mutableStateOf`) + audit doc (comptes 223 résiduels §14/#64 et §16.ter → 259 ; §9 et §20.bis à jour). Web NON concerné (miroir lab-timing laissé tel quel — boucle correcte et testée ; `importJson` par lots reste en dette). 259 tests JVM + 14 UI + lint verts. **Publication** : commit d531eca + tag v1.10.1 poussés sur les 2 Gitea, APK release (2,7 Mo) + debug (21 Mo) publiés et vérifiés par téléchargement (cloudyfy id 78, farewell id 1588586) ; validation §16.ter sur l'APK RELEASE AVANT tag (accueil + données, singleTop, planned_at A/B, 0 crash). | | 28 sept. 2026 (session v1.10.1) | **Durcissement rappels — revue de code post-v1.10.0** (audit demandé : « optimisations ou problèmes, web et Android »). (a) **`launchMode="singleTop"`** : le combo `standard`+`CLEAR_TOP` recréait l'activité au tap notification app-ouverte et rendait le `onNewIntent` de la v1.10.0 inopérant (code mort) — désormais délivré SANS recreation (état UI préservé). (b) **`EXTRA_PLANNED_AT`** : le garde « créneau déjà honoré » évaluait le jour au moment du FEU — une alarme inexacte (`setWindow`/Doze) glissant 23:58→00:02 évaluait le mauvais jour ; l'instant planifié voyage dans l'intent (scheduleFor), le snooze le FORWARDE, le garde évalue le jour du CRÉNEAU ; vérifié émulateur (broadcast `planned_at` hier → notif sautée). (c) **`BootReceiver` consolidé** (dette §20.bis #2 FAIT) : `goAsync`+coroutine, `runBlocking` supprimés. (d) Style (import `mutableStateOf`) + audit doc (comptes 223 résiduels §14/#64 et §16.ter → 259 ; §9 et §20.bis à jour). Web NON concerné (miroir lab-timing laissé tel quel — boucle correcte et testée ; `importJson` par lots reste en dette). 259 tests JVM + 14 UI + lint verts. **Publication** : commit d531eca + tag v1.10.1 poussés sur les 2 Gitea, APK release (2,7 Mo) + debug (21 Mo) publiés et vérifiés par téléchargement (cloudyfy id 78, farewell id 1588586) ; validation §16.ter sur l'APK RELEASE AVANT tag (accueil + données, singleTop, planned_at A/B, 0 crash). |
| 27-28 sept. 2026 (session v1.10.0) | **2 fixes + 1 amélioration rappels** (remontées + nouvelles données réelles → 6ᵉ régression épinglée `backup-v1.9.8.json`). (a) **#68 — reco de prise de sang antérieure à la stabilisation** : le « creux » = minimum de la FENÊTRE ENTIÈRE tombait juste après l'injection PRÉCÉDENTE pour les esters à montée lente (EEn pic ~J+5 ≈ intervalle 7 j → creux d'absorption ~3 h post-injection) — date gouvernée par l'injection N−1 (antérieure à la stab) et « juste avant ton injection du … » faux de plusieurs jours (réel : creux 5 oct 02:32 < stab 7 oct 04:03). FIX : creux = niveau PRÉ-INJECTION du créneau (restaure le principe v1.8.0 documenté ; EV inchangé — le point pré-injection EST son min de fenêtre) ; **miroir web appliqué** (lab-timing.js). Tests : RegressionUserCase6Test data-driven dont balayage de `now` ±60 j (invariant « creux ≥ stab » à TOUT INSTANT) ; les 17 LabTimingTest passent inchangés. (b) **#69 — dialog fantôme** : tap notification → dialog ; le fermer + changer d'onglet + revenir sur l'accueil le ROUVRAIT à chaque fois (paramètre d'activité vs consommation dans les remember de Home, détruits à chaque sortie d'onglet — reproduit sur émulateur AVANT fix). FIX : `MutableState<Long?>` détenu par MainActivity, consommé UNE fois (extras retirés de l'intent après lecture ; bonus : tap notification app-ouverte ouvre aussi le dialog). Test UI `LogDoseRequestTest` + vérification émulateur. (c) **Rappel sauté si dose déjà loggée** (demande) : garde au déclenchement — `hasDoseLoggedOnDay` (pur, jour calendaire LOCAL) dans ReminderReceiver → notif sautée + re-programmation créneau suivant dans tous les cas ; 6 tests JVM + vérification A/B émulateur (EEn dose-du-jour → 0 notif, Fluoxetine sans dose → 1 notif). **259 tests JVM + 14 UI + lint** ; **web 180 tests + E2E verts** (check.sh). **Publication** : commit a4f859f + tag v1.10.0 poussés sur les 2 Gitea, APK release (2,7 Mo) + debug (21 Mo) publiés et vérifiés par téléchargement (cloudyfy id 77, farewell id 1588583). **Web miroir v1.10.0 sync** (fix #68 lab-timing, WEB_VERSION alignée, commit b865b16 + tag poussés ; #69/garde rappel = Android-only, notés dans le changelog web). | | 27-28 sept. 2026 (session v1.10.0) | **2 fixes + 1 amélioration rappels** (remontées + nouvelles données réelles → 6ᵉ régression épinglée `backup-v1.9.8.json`). (a) **#68 — reco de prise de sang antérieure à la stabilisation** : le « creux » = minimum de la FENÊTRE ENTIÈRE tombait juste après l'injection PRÉCÉDENTE pour les esters à montée lente (EEn pic ~J+5 ≈ intervalle 7 j → creux d'absorption ~3 h post-injection) — date gouvernée par l'injection N−1 (antérieure à la stab) et « juste avant ton injection du … » faux de plusieurs jours (réel : creux 5 oct 02:32 < stab 7 oct 04:03). FIX : creux = niveau PRÉ-INJECTION du créneau (restaure le principe v1.8.0 documenté ; EV inchangé — le point pré-injection EST son min de fenêtre) ; **miroir web appliqué** (lab-timing.js). Tests : RegressionUserCase6Test data-driven dont balayage de `now` ±60 j (invariant « creux ≥ stab » à TOUT INSTANT) ; les 17 LabTimingTest passent inchangés. (b) **#69 — dialog fantôme** : tap notification → dialog ; le fermer + changer d'onglet + revenir sur l'accueil le ROUVRAIT à chaque fois (paramètre d'activité vs consommation dans les remember de Home, détruits à chaque sortie d'onglet — reproduit sur émulateur AVANT fix). FIX : `MutableState<Long?>` détenu par MainActivity, consommé UNE fois (extras retirés de l'intent après lecture ; bonus : tap notification app-ouverte ouvre aussi le dialog). Test UI `LogDoseRequestTest` + vérification émulateur. (c) **Rappel sauté si dose déjà loggée** (demande) : garde au déclenchement — `hasDoseLoggedOnDay` (pur, jour calendaire LOCAL) dans ReminderReceiver → notif sautée + re-programmation créneau suivant dans tous les cas ; 6 tests JVM + vérification A/B émulateur (EEn dose-du-jour → 0 notif, Fluoxetine sans dose → 1 notif). **259 tests JVM + 14 UI + lint** ; **web 180 tests + E2E verts** (check.sh). **Publication** : commit a4f859f + tag v1.10.0 poussés sur les 2 Gitea, APK release (2,7 Mo) + debug (21 Mo) publiés et vérifiés par téléchargement (cloudyfy id 77, farewell id 1588583). **Web miroir v1.10.0 sync** (fix #68 lab-timing, WEB_VERSION alignée, commit b865b16 + tag poussés ; #69/garde rappel = Android-only, notés dans le changelog web). |
| 22 sept. 2026 (session v1.9.8) | **Socle qualité : tests UI Compose + signature release dédiée + découpage Settings** (revue de code du 21 sept. : « pas de CI, pas de tests UI, APK signé clé debug, écrans qui grandissent »). (a) **13 tests UI instrumentés** (`app/src/androidTest/uitest/`, §8.ter) : navigation 5 onglets (preuves = éléments inconditionnels : chips 24 h, FAB), dialog Nouveautés (épinglage du fix v1.3.2 : ne réapparaît pas après dismiss+restart), sections Paramètres + version installée = BuildConfig (le bug #46 ne peut plus revenir) ; déterminisme = pm grant shell pré-launch + markChangelogSeen + assertions via targetContext (indépendant FR/EN) + zéro dépendance aux données Room ; deps androidTest (BOM + ui-test-junit4 + ext:junit-ktx + junit explicite). (b) **Signature release conditionnelle** (§16.quater) : `scripts/make-release-keystore.sh` (shellcheck+shfmt) génère keystore + keystore.properties (gitignorés), `assembleRelease` l'utilise s'il existe, sinon fallback clé debug inchangé ; migration backup→désinstall→réimport documentée. (c) **SettingsScreen découpé** (840 → 377 + 11 fichiers 33–164 lignes dans `ui/screens/settings/`, §10) — déplacement pur, commentaires historiques conservés, ChartScreen volontairement entier (garde de source #54). **Vérifications** : 249 tests JVM + 13 UI verts, lint vert, assembleDebug/Release OK, **smoke test RELEASE émulateur** (accueil, 5 onglets, Paramètres complet scroll compris — version 1.9.8 visible, éditeur traitement, dialog dose + garde « crée un traitement », 0 crash logcat — anti-#64). **README EN** (README.en.md, liens FR/EN croisés ; DEVELOPPEMENT.md reste FR par choix assumé). **Publication** : commit 09cddfc + tag v1.9.8 poussés sur les 2 Gitea, APK release (2,7 Mo) + debug (21 Mo) publiés et vérifiés par téléchargement (cloudyfy id 75, farewell id 1588581). Web miroir NON concerné (v1.9.8 = socle Android uniquement : tests instrumentés, signature APK, découpage Compose, README du dépôt Android — rien à porter côté web, qui reste à v1.9.7). | | 22 sept. 2026 (session v1.9.8) | **Socle qualité : tests UI Compose + signature release dédiée + découpage Settings** (revue de code du 21 sept. : « pas de CI, pas de tests UI, APK signé clé debug, écrans qui grandissent »). (a) **13 tests UI instrumentés** (`app/src/androidTest/uitest/`, §8.ter) : navigation 5 onglets (preuves = éléments inconditionnels : chips 24 h, FAB), dialog Nouveautés (épinglage du fix v1.3.2 : ne réapparaît pas après dismiss+restart), sections Paramètres + version installée = BuildConfig (le bug #46 ne peut plus revenir) ; déterminisme = pm grant shell pré-launch + markChangelogSeen + assertions via targetContext (indépendant FR/EN) + zéro dépendance aux données Room ; deps androidTest (BOM + ui-test-junit4 + ext:junit-ktx + junit explicite). (b) **Signature release conditionnelle** (§16.quater) : `scripts/make-release-keystore.sh` (shellcheck+shfmt) génère keystore + keystore.properties (gitignorés), `assembleRelease` l'utilise s'il existe, sinon fallback clé debug inchangé ; migration backup→désinstall→réimport documentée. (c) **SettingsScreen découpé** (840 → 377 + 11 fichiers 33–164 lignes dans `ui/screens/settings/`, §10) — déplacement pur, commentaires historiques conservés, ChartScreen volontairement entier (garde de source #54). **Vérifications** : 249 tests JVM + 13 UI verts, lint vert, assembleDebug/Release OK, **smoke test RELEASE émulateur** (accueil, 5 onglets, Paramètres complet scroll compris — version 1.9.8 visible, éditeur traitement, dialog dose + garde « crée un traitement », 0 crash logcat — anti-#64). **README EN** (README.en.md, liens FR/EN croisés ; DEVELOPPEMENT.md reste FR par choix assumé). **Publication** : commit 09cddfc + tag v1.9.8 poussés sur les 2 Gitea, APK release (2,7 Mo) + debug (21 Mo) publiés et vérifiés par téléchargement (cloudyfy id 75, farewell id 1588581). Web miroir NON concerné (v1.9.8 = socle Android uniquement : tests instrumentés, signature APK, découpage Compose, README du dépôt Android — rien à porter côté web, qui reste à v1.9.7). |
@ -754,7 +755,7 @@ devient à son tour**.
## 8. Tests unitaires ## 8. Tests unitaires
**259 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — **225 sans **264 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — **230 sans
les données de test locales** (cf §8.bis : les 6 classes de régression sont les données de test locales** (cf §8.bis : les 6 classes de régression sont
skippées via `Assume`). Dépendance : JUnit 4.13.2. skippées via `Assume`). Dépendance : JUnit 4.13.2.
Emplacement : `app/src/test/java/com/hormonetrack/`. Répertoire de travail d'exécution = Emplacement : `app/src/test/java/com/hormonetrack/`. Répertoire de travail d'exécution =
@ -987,9 +988,9 @@ dépôt (ni maintenant, ni si le repo devient public un jour).
dans le code (le scanner refuserait, et ce serait une fuite). dans le code (le scanner refuserait, et ce serait une fuite).
- les tests font `Assume.assumeTrue(file.exists())` dans le `@Before` : **sans le - 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 fichier, la classe est IGNORÉE** (skipped, pas failed) — un clone neuf ou une CI
exécute 225 tests au lieu de 259 (v1.10.0 ; chiffres historiques : 44/87 à v1.3.x, exécute 230 tests au lieu de 264 (v1.11.0 ; 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, 150/172 à v1.4.x, 160/183 à v1.5.0, 168/191 à v1.6.0, 177/200 à v1.7.0,
181/211 à v1.7.1, 190/220 à v1.8.0, 219/249 à v1.9.x) ; 181/211 à v1.7.1, 190/220 à v1.8.0, 219/249 à v1.9.x, 225/259 à v1.10.x) ;
- le workdir des tests Gradle est le dossier du module (`app/`) → les tests - le workdir des tests Gradle est le dossier du module (`app/`) → les tests
cherchent les fichiers à plusieurs chemins (`../local-test-data/…` en premier) ; cherchent les fichiers à plusieurs chemins (`../local-test-data/…` en premier) ;
- **pour les lancer** : exporter un backup JSON depuis l'app → l'enregistrer sous - **pour les lancer** : exporter un backup JSON depuis l'app → l'enregistrer sous
@ -1199,7 +1200,12 @@ Sur la montre : remontée par Gadgetbridge **ou** Huawei Health (cf §17).
les valeurs — choix de design : les labs sont prélevés ensemble) ; suggestions 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 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 / ⏰, - `TreatmentsScreen` : cartes (nom, route, dose, chips ester·modèle / Tmax / ×scale / ⏰,
badge inactif), FAB → éditeur badge inactif), FAB → éditeur. **v1.11.0** : ordre d'affichage =
`treatmentsForDisplay` (pur, tri STABLE — actifs d'abord, inactifs
regroupés TOUT EN BAS sous un en-tête `inactive_section` qui n'apparaît
que s'il existe des inactifs) + cartes inactives **atténuées** (alpha
0,55) — 5 tests `TreatmentsDisplayTest` ; miroir web
(`js/data/models.js` + `tests/treatments-display.test.js`)
- `TreatmentEditorScreen` : 12 presets (`PKPresets`, cf `nameRes`) pré-remplissent tout ; - `TreatmentEditorScreen` : 12 presets (`PKPresets`, cf `nameRes`) pré-remplissent tout ;
champs conditionnels (ester+modèle si injection, Bateman sinon) ; carte Calibration champs conditionnels (ester+modèle si injection, Bateman sinon) ; carte Calibration
(scaleFactor + « Calibrer avec les analyses ») ; **section « Posologie » (v1.2 : (scaleFactor + « Calibrer avec les analyses ») ; **section « Posologie » (v1.2 :
@ -2036,8 +2042,8 @@ de signature est requise — elle est désormais câblée :
1. **Bumper la version** dans `app/build.gradle.kts` : `versionCode = N+1`, 1. **Bumper la version** dans `app/build.gradle.kts` : `versionCode = N+1`,
`versionName = "X.Y.Z+1"` (SemVer : fix = Z, feature = Y). `versionName = "X.Y.Z+1"` (SemVer : fix = Z, feature = Y).
2. **Tests verts obligatoires** : `./gradlew testDebugUnitTest` — 259 au 2. **Tests verts obligatoires** : `./gradlew testDebugUnitTest` — 264 au
total, 225 si `local-test-data/` est absent (les 6 classes de régression total, 230 si `local-test-data/` est absent (les 6 classes de régression
réelles sont skippées via `Assume`) ; **lint vert réelles sont skippées via `Assume`) ; **lint vert
obligatoire** : `./gradlew lint` (v1.3.4 — `StringFormatMatches` aurait obligatoire** : `./gradlew lint` (v1.3.4 — `StringFormatMatches` aurait
attrapé les crashs #47/#48 dès v1.3.1). attrapé les crashs #47/#48 dès v1.3.1).
@ -2334,6 +2340,9 @@ Sur le téléphone de test (à compléter par l'utilisatrice) :
cohérente avec le créneau affiché (~juste avant) cohérente avec le créneau affiché (~juste avant)
- [ ] **v1.10.0** : logger la dose du jour AVANT l'heure de rappel → la - [ ] **v1.10.0** : logger la dose du jour AVANT l'heure de rappel → la
notification ne sonne pas ; le rappel suivant repart au créneau suivant notification ne sonne pas ; le rappel suivant repart au créneau suivant
- [ ] **v1.11.0** : page Traitements — les inactifs sont TOUT EN BAS sous
l'en-tête « Traitements inactifs », cartes atténuées ; éditer un
inactif fonctionne toujours
- [ ] **v1.2.x** : graphique panoramique (glisser → passé, bouton « Revenir à maintenant ») - [ ] **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** : 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 « Prévision » (configurer la Posologie d'un traitement d'abord)