v1.9.6 : la carte Accueil liste TOUTES les prochaines doses (triées)
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
This commit is contained in:
parent
0f0ecc3d23
commit
4418429fa7
@ -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 <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 # 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
|
||||
```
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -458,48 +458,76 @@ private fun NowLevelCard(curve: List<LevelPoint>) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<Treatment>, doseLogs: List<DoseLog>) {
|
||||
// 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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
<string name="no_data">Pas encore de données — crée un traitement et logue une dose.</string>
|
||||
<string name="logged_today">Log rapide</string>
|
||||
<string name="next_dose">Prochaine dose</string>
|
||||
<string name="next_doses">Prochaines doses</string>
|
||||
<string name="next_dose_days">%1$d j %2$d h · %3$s (%4$s)</string>
|
||||
<string name="delta_6h">%1$s %2$s pg/mL vs il y a 6 h</string>
|
||||
<string name="home_chart_title">Dernières 24 h (estimation)</string>
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
<string name="no_data">No data yet — add a treatment and log a dose.</string>
|
||||
<string name="logged_today">Quick log</string>
|
||||
<string name="next_dose">Next dose</string>
|
||||
<string name="next_doses">Upcoming doses</string>
|
||||
<string name="next_dose_days">%1$d d %2$d h · %3$s (%4$s)</string>
|
||||
<string name="delta_6h">%1$s %2$s pg/mL vs 6 h ago</string>
|
||||
<string name="home_chart_title">Last 24 hours (estimate)</string>
|
||||
|
||||
45
app/src/test/java/com/hormonetrack/ui/NextDoseLineTest.kt
Normal file
45
app/src/test/java/com/hormonetrack/ui/NextDoseLineTest.kt
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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. |
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user