From a40d9bc076c58d2847782c5da130a1fe5a47be39 Mon Sep 17 00:00:00 2001 From: Siphonight Date: Mon, 21 Sep 2026 21:56:32 +0200 Subject: [PATCH] =?UTF-8?q?v1.9.6=20:=20la=20carte=20Accueil=20liste=20TOU?= =?UTF-8?q?TES=20les=20prochaines=20doses=20(tri=C3=A9es)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remontée : le rappel quotidien (CPA) masquait le rappel hebdomadaire (EEn) — « je ne le vois quasiment pas ». - NextDoseCard : UNE ligne par traitement, TRIÉES par prochaine prise ; 1ʳᵉ ligne (la plus proche) en avant, suivantes en style secondaire ; titre pluriel « Prochaines doses » dès 2 lignes (next_doses FR/EN) - Helper PUR nextDoseLine (locale/zone paramétrables, template jours localisé next_dose_days) + 3 tests NextDoseLineTest - Notifications inchangées (une alarme par traitement) - versionCode 43 / 1.9.6 + CHANGELOG/§2/README/GUIDE --- README.md | 4 +- app/build.gradle.kts | 4 +- .../com/hormonetrack/ui/screens/HomeScreen.kt | 82 +++++++++++++------ app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + .../com/hormonetrack/ui/NextDoseLineTest.kt | 45 ++++++++++ docs/CHANGELOG.md | 21 +++++ docs/DEVELOPPEMENT.md | 1 + docs/GUIDE_INSTALLATION.md | 6 +- 9 files changed, 133 insertions(+), 32 deletions(-) create mode 100644 app/src/test/java/com/hormonetrack/ui/NextDoseLineTest.kt diff --git a/README.md b/README.md index aa5ee19..0d81572 100644 --- a/README.md +++ b/README.md @@ -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.9.5 — build Android ✅, **lint vert** ✅, **244 tests unitaires** ✅ (214 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.9.6 — build Android ✅, **lint vert** ✅, **247 tests unitaires** ✅ (217 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) @@ -123,7 +123,7 @@ par AGP si les licences sont signées). Le wrapper télécharge Gradle 9.7.1. git clone && 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 # 244 tests (214 sans les données locales) +./gradlew testDebugUnitTest # 247 tests (217 sans les données locales) ./gradlew lint # lint vert obligatoire avant release ``` diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 52e5963..217e12b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 = 42 - versionName = "1.9.5" + versionCode = 43 + versionName = "1.9.6" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/java/com/hormonetrack/ui/screens/HomeScreen.kt b/app/src/main/java/com/hormonetrack/ui/screens/HomeScreen.kt index ce14829..06ec016 100644 --- a/app/src/main/java/com/hormonetrack/ui/screens/HomeScreen.kt +++ b/app/src/main/java/com/hormonetrack/ui/screens/HomeScreen.kt @@ -458,48 +458,76 @@ private fun NowLevelCard(curve: List) { } } +/** + * Ligne « prochaine prise » d'un traitement (v1.9.6 — PUR, testable JVM) : + * « Xj Yh · ven. 25 10:00 (nom) » au-delà de 24 h (les heures deviennent peu + * lisibles, v1.4.1), « XhMM · HH:mm (nom) » en dessous, « N min · HH:mm + * (nom) » dans l'heure. Utilisée par chaque ligne de [NextDoseCard]. + */ +internal fun nextDoseLine( + nowMs: Long, + fireMs: Long, + name: String, + /** Template localisé du cas ≥ 24 h (string `next_dose_days`, 4 args). */ + daysTemplate: String, + zone: ZoneId = ZoneId.systemDefault(), + locale: Locale = Locale.getDefault() +): String { + val deltaMs = fireMs - nowMs + val h = deltaMs / HOUR_MS + val m = (deltaMs % HOUR_MS) / 60_000L + return if (h >= 24) { + val (days, remH) = com.hormonetrack.util.HrtDuration.daysAndHours(h) + val dayTimeStr = DateTimeFormatter.ofPattern("EEE d HH:mm") + .withZone(zone) + .withLocale(locale) + .format(Instant.ofEpochMilli(fireMs)) + String.format(locale, daysTemplate, days, remH, dayTimeStr, name) + } else { + val timeStr = DateTimeFormatter.ofPattern("HH:mm") + .withZone(zone) + .withLocale(locale) + .format(Instant.ofEpochMilli(fireMs)) + if (h > 0) { + String.format(locale, "%dh%02d · %s (%s)", h, m, timeStr, name) + } else { + String.format(locale, "%d min · %s (%s)", m, timeStr, name) + } + } +} + @Composable private fun NextDoseCard(treatments: List, doseLogs: List) { // v1.4.0 (fix #52) : les dates de rappel passent par le moteur — la carte // suit la grille Posologie (jour du créneau uniquement) comme les alarmes. + // v1.9.6 (demande) : la carte liste TOUTES les prochaines prises, UNE + // ligne par traitement, TRIÉES par prochaine prise — l'ancienne carte ne + // montrait que la plus proche, et une prise quotidienne (CPA) masquait + // totalement le rappel de l'injection hebdomadaire (le rappel EEn + // « n'arrivait qu'après celui de la CPA »). Le titre passe au pluriel + // dès qu'il y a au moins deux lignes. val nowMs = System.currentTimeMillis() val fires = treatments .mapNotNull { tr -> PharmacokineticEngine.nextReminderFireFor(tr, doseLogs, nowMs)?.let { tr to it } } + .sortedBy { it.second } if (fires.isEmpty()) return - val (nextTreatment, next) = fires.minByOrNull { it.second }!! Card(Modifier.fillMaxWidth()) { Column(Modifier.padding(16.dp)) { Text( - stringResource(R.string.next_dose), + stringResource(if (fires.size == 1) R.string.next_dose else R.string.next_doses), style = MaterialTheme.typography.labelMedium ) Spacer(Modifier.height(4.dp)) - val deltaMs = next - System.currentTimeMillis() - val h = deltaMs / HOUR_MS - val m = (deltaMs % HOUR_MS) / 60_000L - val name = nextTreatment?.name ?: "" - // v1.4.1 : au-delà de 24 h, afficher en JOURS (les heures - // deviennent peu lisibles) + le jour de la semaine dans la date. - if (h >= 24) { - val (days, remH) = com.hormonetrack.util.HrtDuration.daysAndHours(h) - val dayTimeStr = DateTimeFormatter.ofPattern("EEE d HH:mm") - .withZone(ZoneId.systemDefault()) - .format(Instant.ofEpochMilli(next)) + // Première ligne (la plus proche) mise en avant, les suivantes en + // style secondaire — même hiérarchie visuelle que v1.4.x. + fires.forEachIndexed { idx, (treatment, next) -> Text( - stringResource(R.string.next_dose_days, days, remH, dayTimeStr, name), - style = MaterialTheme.typography.titleMedium - ) - } else { - val timeStr = DateTimeFormatter.ofPattern("HH:mm") - .withZone(ZoneId.systemDefault()) - .format(Instant.ofEpochMilli(next)) - Text( - if (h > 0) { - String.format(Locale.getDefault(), "%dh%02d · %s (%s)", h, m, timeStr, name) - } else { - String.format(Locale.getDefault(), "%d min · %s (%s)", m, timeStr, name) - }, - style = MaterialTheme.typography.titleMedium + nextDoseLine( + nowMs, next, treatment?.name ?: "", + stringResource(R.string.next_dose_days) + ), + style = if (idx == 0) MaterialTheme.typography.titleMedium + else MaterialTheme.typography.bodyMedium ) } } diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 1ba528a..47f28b0 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -17,6 +17,7 @@ Pas encore de données — crée un traitement et logue une dose. Log rapide Prochaine dose + Prochaines doses %1$d j %2$d h · %3$s (%4$s) %1$s %2$s pg/mL vs il y a 6 h Dernières 24 h (estimation) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fabcf62..f41f345 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -17,6 +17,7 @@ No data yet — add a treatment and log a dose. Quick log Next dose + Upcoming doses %1$d d %2$d h · %3$s (%4$s) %1$s %2$s pg/mL vs 6 h ago Last 24 hours (estimate) diff --git a/app/src/test/java/com/hormonetrack/ui/NextDoseLineTest.kt b/app/src/test/java/com/hormonetrack/ui/NextDoseLineTest.kt new file mode 100644 index 0000000..524fd15 --- /dev/null +++ b/app/src/test/java/com/hormonetrack/ui/NextDoseLineTest.kt @@ -0,0 +1,45 @@ +package com.hormonetrack.ui + +import com.hormonetrack.ui.screens.nextDoseLine +import org.junit.Assert.assertEquals +import org.junit.Test +import java.time.ZoneId +import java.time.ZonedDateTime +import java.util.Locale + +/** + * Tests du helper PUR `nextDoseLine` (v1.9.6 — carte « Prochaines doses » de + * l'Accueil, une ligne par traitement triée par prochaine prise). Zone et + * locale FIXES pour un format déterministe. + */ +class NextDoseLineTest { + + private val zone = ZoneId.of("Europe/Paris") + private val locale = Locale.US + // Template du cas ≥ 24 h (string EN `next_dose_days`) + private val daysTemplate = "%1\$d d %2\$d h · %3\$s (%4\$s)" + + private fun at(hoursFromNow: Long): Long = + ZonedDateTime.of(2026, 9, 21, 12, 0, 0, 0, zone).toInstant().toEpochMilli() + hoursFromNow * 3_600_000L + + @Test + fun `au-dela de 24 h - jours + jour de semaine`() { + // now = lundi 21/09 12:00 → fire = samedi 26/09 10:00 (118 h) + val line = nextDoseLine(at(0), at(118), "Injection EEn 5mg 7d", daysTemplate, zone, locale) + assertEquals("4 d 22 h · Sat 26 10:00 (Injection EEn 5mg 7d)", line) + } + + @Test + fun `entre 1 h et 24 h - heures et minutes`() { + // now = 12:00 → fire = 14:05 (2 h 05) + val line = nextDoseLine(at(0), at(2) + 5 * 60_000L, "CPA", daysTemplate, zone, locale) + assertEquals("2h05 · 14:05 (CPA)", line) + } + + @Test + fun `moins d'une heure - minutes`() { + // now = 12:00 → fire = 12:07 + val line = nextDoseLine(at(0), at(0) + 7 * 60_000L, "CPA", daysTemplate, zone, locale) + assertEquals("7 min · 12:07 (CPA)", line) + } +} diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 0859b62..593f7bc 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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.9.6] — 2026-09-20 (versionCode 43) + +### Modifié — la carte de l'Accueil liste TOUTES les prochaines prises + +- **Remontée** : « si une injection et une prise d'un autre médicament ont + lieu en même temps… le rappel ne montre que la prise qui vient, non + toutes les prises du jour » — avec une prise quotidienne (CPA), le + rappel de l'injection hebdomadaire (EEn) « n'arrive qu'après celui de la + CPA, du coup je ne le vois quasiment pas ». +- **Fix** : la carte « Prochaine dose » devient « **Prochaines doses** » + (titre pluriel dès 2 lignes) — **une ligne par traitement, TRIÉES par + prochaine prise** ; la 1ʳᵉ ligne (la plus proche) reste mise en avant. + Format de ligne inchangé (`Xj Yh · ven. 25 10:00 (nom)`, `XhMM · HH:mm`, + `N min`). Le CPA se lit désormais AU-DESSUS de l'EEn du samedi, jamais à + sa place. +- Les NOTIFICATIONS de rappel sont inchangées (une alarme par traitement + — le problème était purement l'affichage de l'Accueil). +- Moteur : helper PUR `nextDoseLine` (HomeScreen.kt, locale/zone + paramétrables, template jours localisé) — 3 tests (NextDoseLineTest). + 247 tests verts + lint. + ## [1.9.5] — 2026-09-20 (versionCode 42) ### Corrigé — revue de code post-v1.9.4 (audit lecture-seule des deux dépôts) diff --git a/docs/DEVELOPPEMENT.md b/docs/DEVELOPPEMENT.md index 618b46b..cacfcfb 100644 --- a/docs/DEVELOPPEMENT.md +++ b/docs/DEVELOPPEMENT.md @@ -63,6 +63,7 @@ Fonctionnalités v1 : | Date | Événement | |---|---| +| 20 sept. 2026 (session v1.9.6) | **Carte Accueil « Prochaines doses » — toutes les prises, triées** (remontée : le rappel quotidien CPA masquait le rappel hebdomadaire EEn — « je ne le vois quasiment pas ») : la carte liste UNE ligne par traitement TRIÉES par prochaine prise (1ʳᵉ en avant, suivantes en style secondaire), titre pluriel dès 2 lignes (`next_doses` FR/EN) ; helper PUR `nextDoseLine` (locale/zone paramétrables, template jours localisé `next_dose_days`) — 3 tests `NextDoseLineTest`. Notifications inchangées (une alarme par traitement). Web miroir v1.9.6 : helper pur `nextDoseLineText` (format.js) + même carte + 3 tests miroirs (misc) — 247 verts Android / 178 web. | | 20 sept. 2026 (session v1.9.5) | **Revue de code post-v1.9.4 — audit lecture-seule des deux dépôts puis corrections** (demande : « les logiques, choix techniques, commentaires, optimisations ») : (a) **#67** : ticks X alignés sur le fuseau APPAREIL pendant que les labels étaient formatés dans le fuseau CHOISI (zone manquante à `xLabelTicks`, contredisant #55) + pas fixe `+24 h` dérivant d'1 h aux transitions DST → zone passée + avancement Calendar, test DST (Europe/Paris 29/03) ; (b) `EstrannaiseCloud.compute` : `nowMs` mort supprimé (KDoc mensonger) ; (c) **perf** : `E2Context`/`prepareE2Context` — le tracé labs (720 pts) reconstruisait les maps moteur par point, diluant v1.8.2 (résultat bit-identique) ; Paints texte + `detectExtrema` + formatters mémoïsés dans le Canvas ; (d) **règles ré-appliquées** : `modelLabelRes` (anti-#59), `PKModels.*` partout (fini les literals), `scheduler2` supprimé ; (e) **doc rattrapée** : 3 commentaires périmés (ODS/lookup/t½-table), §7.2/§7.3 (asset historique, cutoff 10×t½). **Web miroir v1.9.5** : garde `hasModel` ESE dans `cutoffHours` (un backup importé ESE+ester non couvert levait TypeError → écran vide), portage des optimisations v1.8.2 (contexte préparé, cutoffs par traitement, Bateman paresseux), min/max en une passe (RangeError possible sur ~230 k pts avec le Nuage), init MCMC non bloquante (data-ready ⇒ asset chargé), `pk_profiles.json` exclu du zip (export-ignore, −548 Ko livrés), E2E version paramétrée (`data-version` — anti-bug v1.9.3), nettoyages (import mort lab-timing, JSDoc « troncature », dialogs ESE=3, COLORS accueil, JSDoc orpheline). 244 verts Android + lint · 175 verts web + E2E ✓. | | 19 sept. 2026 (session v1.9.4) | **Presets du modèle ESE étendus à ses 6 esters injectables** (remontée : « depuis l'ajout du modèle source ESE, les presets n'ont pas été mis à jour ») : 3 presets ajoutés (EC huile 5 mg IM, EB 1 mg IM, EUCS suspension cristalline 10 mg SC — 25 entrées), champs Bateman informatifs = t½ des fits ; **éditeur** : la liste des esters suit `Esters.choicesForModel` (parité DoseDialog/web) et rebascule sur EV si l'ester sort de la couverture au changement de modèle ; **web** : 2 trous du portage v1.9.0 corrigés (`Esters.EUCS` manquant dans pk-engine.js — preset référençant `undefined` — et `choicesForModel` resté sur la couverture ODS à 3 esters) + `tests/presets.test.js` (6 tests, miroir `PKPresetsTest`). 243 verts / 213 sans données locales + lint + check web (175). Doc : doublon §7.12 supprimé. Web v1.9.4 sync. | | 19 sept. 2026 (session v1.9.2) | **Fix nuage vide sans traitement stocké ESE** (remontée : « il ne s'active que autour du tracé émulé, pas autour du modèle Estrannaise ») : le filtre du nuage testait le pkModel STOCKÉ — or la courbe ESE redessine TOUTES les doses E2 (modelOverride ESE) quel que soit le modèle stocké. FIX : le nuage couvre les doses à ester effectif couvert par ESE (peu importe le stocké) ; l'exclusivité reste portée par le chip ; l'oral Bateman reste hors nuage. 2 tests réécrits (237 verts / 207 sans). Web miroir v1.9.2. | diff --git a/docs/GUIDE_INSTALLATION.md b/docs/GUIDE_INSTALLATION.md index 46a93e4..2bc4e01 100644 --- a/docs/GUIDE_INSTALLATION.md +++ b/docs/GUIDE_INSTALLATION.md @@ -105,7 +105,11 @@ Les notifications de l'app remontent automatiquement sur la montre via **Huawei - Ester (EV / EU / EEn) + modèle (Estrannaise / Transfem Science) = les courbes du `.ods` - Pour gel/patch/oral : paramètres Bateman (temps au pic, demi-vie, biodispo) 2. **Doses → +** → logue tes injections passées - - L'accueil affiche la **prochaine dose en jours** quand elle est à plus de 24 h + - L'accueil liste **toutes les prochaines doses** (v1.9.6) : une ligne + par traitement, triées par prochaine prise (la plus proche en premier, + en gras) — une prise quotidienne ne masque plus le rappel de + l'injection hebdomadaire. Affichage en **jours** quand la prochaine + prise est à plus de 24 h (ex. « 5 j 2 h · sam. 6 18:00 »), en heures/minutes sinon - La carte « Niveau actuel » compare ton taux estimé **à il y a 6 h** (flèche ↗/↘) — autour d'un ester lent comme l'énanthate, cette variation