v1.4.1 : prévision étendue et sans saut — activer le chip ne déplace plus la fenêtre (endMs = now − panHours signé, panHours < 0 = futur borné par l'horizon 12×Posologie [30 j ; 1 an], clamp unique clampPanHours) ; delta en jours sur la carte Prochaine dose (> 24 h : '5 j 2 h · sam. 6 18:00') ; 5 tests (112 verts) + lint
This commit is contained in:
parent
c1c2386ab2
commit
922d49841f
13
README.md
13
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
|
> à 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.4.0 — build Android ✅, **lint vert** ✅, **107 tests unitaires** ✅ (89 sans les données de test locales ; 3 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.4.1 — build Android ✅, **lint vert** ✅, **112 tests unitaires** ✅ (94 sans les données de test locales ; 3 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)
|
- **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)
|
||||||
@ -38,7 +38,9 @@ sauvegarde JSON. **100 % local, aucun compte, aucun serveur.**
|
|||||||
- **Modèle Bateman** paramétrable (temps au pic, demi-vie, biodisponibilité) pour gel,
|
- **Modèle Bateman** paramétrable (temps au pic, demi-vie, biodisponibilité) pour gel,
|
||||||
patch et voie orale
|
patch et voie orale
|
||||||
- **Simulation prévisionnelle** : configurer la **posologie** (intervalle en jours) sur un
|
- **Simulation prévisionnelle** : configurer la **posologie** (intervalle en jours) sur un
|
||||||
traitement → projection des doses à venir sur le graphique (jamais sauvegardées)
|
traitement → projection des doses à venir sur le graphique (jamais sauvegardées) ;
|
||||||
|
**l'activation ne déplace pas le graphique** et la projection se parcourt en tirant
|
||||||
|
vers la gauche, jusqu'à **1 an** (v1.4.1)
|
||||||
- **Log des doses** avec date/heure exacte, dose en mg, **ester par injection**
|
- **Log des doses** avec date/heure exacte, dose en mg, **ester par injection**
|
||||||
(switch EV↔EU↔EEn comme dans le tableur), **éditable** (tap sur une ligne dans Doses),
|
(switch EV↔EU↔EEn comme dans le tableur), **éditable** (tap sur une ligne dans Doses),
|
||||||
**intervalle en jours entre dosages** affiché
|
**intervalle en jours entre dosages** affiché
|
||||||
@ -62,7 +64,8 @@ sauvegarde JSON. **100 % local, aucun compte, aucun serveur.**
|
|||||||
7 jours ne sonne que le jour d'injection, pas tous les jours
|
7 jours ne sonne que le jour d'injection, pas tous les jours
|
||||||
- **Nouveautés à chaque mise à jour** : dialog de changelog automatique
|
- **Nouveautés à chaque mise à jour** : dialog de changelog automatique
|
||||||
(fermé = ne réapparaît pas avant la prochaine version)
|
(fermé = ne réapparaît pas avant la prochaine version)
|
||||||
- **Temps sous THS** affiché en haut de la page Doses (depuis la 1re prise)
|
- **Temps sous THS** affiché en haut de la page Doses (depuis la 1re prise) ;
|
||||||
|
**prochaine dose en jours** sur l'accueil quand elle est à plus de 24 h (v1.4.1)
|
||||||
- **Logs de diagnostic** exportables (Paramètres) — utile pour le support
|
- **Logs de diagnostic** exportables (Paramètres) — utile pour le support
|
||||||
- **Événements d'agenda** : les rappels de prises peuvent créer un événement
|
- **Événements d'agenda** : les rappels de prises peuvent créer un événement
|
||||||
récurrent (posologie) dans un calendrier « HormoneTrack » de ton téléphone
|
récurrent (posologie) dans un calendrier « HormoneTrack » de ton téléphone
|
||||||
@ -79,7 +82,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 # 107 tests (89 sans les données locales)
|
./gradlew testDebugUnitTest # 112 tests (94 sans les données locales)
|
||||||
./gradlew lint # lint vert obligatoire avant release
|
./gradlew lint # lint vert obligatoire avant release
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -91,7 +94,7 @@ Pas de Play Store : l'app est sideloadée. Détails pas-à-pas : [docs/GUIDE_INS
|
|||||||
|
|
||||||
Dépôts : **gitea.cloudyfy.fr** et **gitea.farewell.dev** (miroir) —
|
Dépôts : **gitea.cloudyfy.fr** et **gitea.farewell.dev** (miroir) —
|
||||||
`Siphonight/HormoneTrack` sur les deux (privé), avec
|
`Siphonight/HormoneTrack` sur les deux (privé), avec
|
||||||
**releases taguées** (`v1.1.0` → `v1.4.0`) et **deux APK par release** (depuis v1.2.5)
|
**releases taguées** (`v1.1.0` → `v1.4.1`) et **deux APK par release** (depuis v1.2.5)
|
||||||
(téléchargeables sans compiler, cf [docs/DEVELOPPEMENT.md §16.bis](docs/DEVELOPPEMENT.md)) :
|
(téléchargeables sans compiler, cf [docs/DEVELOPPEMENT.md §16.bis](docs/DEVELOPPEMENT.md)) :
|
||||||
`-release.apk` (**recommandé**, optimisé R8, 2,4 Mo) et `-debug.apk` (20 Mo) :
|
`-release.apk` (**recommandé**, optimisé R8, 2,4 Mo) et `-debug.apk` (20 Mo) :
|
||||||
|
|
||||||
|
|||||||
@ -17,8 +17,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 = 20
|
versionCode = 21
|
||||||
versionName = "1.4.0"
|
versionName = "1.4.1"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|||||||
@ -77,6 +77,29 @@ fun stepForRange(rangeHours: Long): Long = when {
|
|||||||
else -> 3_600_000L // 1 h
|
else -> 3_600_000L // 1 h
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Horizon de PRÉVISION en heures (v1.4.1) : quantité de doses projetées
|
||||||
|
* générées au-delà de « maintenant » quand le chip Prévision est actif.
|
||||||
|
* 12 × la plus grande Posologie, borné [30 j ; 365 j] — « scroller à
|
||||||
|
* l'infini » s'arrête à 1 an (compromis calcul : 365 j / 6 j ≈ 61 doses
|
||||||
|
* projetées superposées, coût O(points × doses) maîtrisé).
|
||||||
|
* @return null si aucun traitement n'a de Posologie (chip désactivé).
|
||||||
|
*/
|
||||||
|
fun forecastHorizonHours(maxIntervalDays: Double?): Long? {
|
||||||
|
if (maxIntervalDays == null || maxIntervalDays <= 0.0) return null
|
||||||
|
return (maxIntervalDays * 24.0 * 12.0).toLong().coerceIn(24L * 30, 24L * 365)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bornes du panoramique (v1.4.1) : la fenêtre peut reculer dans le PASSÉ
|
||||||
|
* jusqu'à [0 ; maxPastHours] et — quand la prévision est active — AVANCER
|
||||||
|
* dans le FUTUR jusqu'à [-futureHorizonHours ; …] (panHours négatif =
|
||||||
|
* fenêtre décalée vers le futur). Unique point de clamp partagé par le
|
||||||
|
* drag et le zoom pour éviter les divergences.
|
||||||
|
*/
|
||||||
|
fun clampPanHours(panHours: Long, futureHorizonHours: Long, maxPastHours: Long): Long =
|
||||||
|
panHours.coerceIn(-futureHorizonHours.coerceAtLeast(0L), maxPastHours.coerceAtLeast(0L))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Graphique Canvas pur, multi-séries :
|
* Graphique Canvas pur, multi-séries :
|
||||||
* - axe gauche = E2 (pg/mL), axe droit = T (ng/mL) ;
|
* - axe gauche = E2 (pg/mL), axe droit = T (ng/mL) ;
|
||||||
|
|||||||
@ -122,16 +122,23 @@ fun ChartScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Horizon de prévision : 2 × le plus grand intervalle configuré (borné 7–30 jours)
|
// Horizon de prévision (v1.4.1) : 12 × la plus grande Posologie, borné
|
||||||
|
// [30 j ; 365 j] (cf forecastHorizonHours dans CurveChart.kt).
|
||||||
val maxIntervalDays = treatments.mapNotNull { it.forecastIntervalDays }.maxOrNull()
|
val maxIntervalDays = treatments.mapNotNull { it.forecastIntervalDays }.maxOrNull()
|
||||||
val forecastHorizonHours = maxIntervalDays?.let { (it * 24 * 2).toLong().coerceIn(DAY_MS / HOUR_MS * 7, DAY_MS / HOUR_MS * 30) }
|
val forecastHorizonHours = com.hormonetrack.ui.components.forecastHorizonHours(maxIntervalDays)
|
||||||
|
|
||||||
// Fenêtre temporelle courante : endMs recule quand on tire vers la droite
|
// ── Fenêtre temporelle (v1.4.1) ──────────────────────────────────────────
|
||||||
|
// endMs = now − panHours (panHours > 0 = passé, < 0 = futur via prévision).
|
||||||
|
// ⚠️ ACTIVER la prévision ne déplace PLUS la fenêtre (ancien bug UX : le
|
||||||
|
// graphique sautait « tout à droite » dans le futur) : la courbe projetée
|
||||||
|
// s'étend au-delà de la fenêtre courante et on la parcourt en tirant vers
|
||||||
|
// la gauche (panHours négatif), borné par l'horizon. La fenêtre reste où
|
||||||
|
// l'utilisateur l'avait laissée.
|
||||||
val nowMs = System.currentTimeMillis()
|
val nowMs = System.currentTimeMillis()
|
||||||
val earliestDoseMs = doseLogs.minOfOrNull { it.timestamp } ?: nowMs
|
val earliestDoseMs = doseLogs.minOfOrNull { it.timestamp } ?: nowMs
|
||||||
val maxPanHours = (((nowMs - earliestDoseMs) / HOUR_MS) + rangeHours / HOUR_MS).coerceAtLeast(0L)
|
val maxPanHours = (((nowMs - earliestDoseMs) / HOUR_MS) + rangeHours / HOUR_MS).coerceAtLeast(0L)
|
||||||
val endMs = nowMs - panHours * HOUR_MS +
|
val futurePanHorizon = if (showForecast) (forecastHorizonHours ?: 0L) else 0L
|
||||||
(if (showForecast) (forecastHorizonHours ?: 0L) * HOUR_MS else 0L)
|
val endMs = nowMs - panHours * HOUR_MS
|
||||||
val startMs = endMs - rangeHours * HOUR_MS
|
val startMs = endMs - rangeHours * HOUR_MS
|
||||||
|
|
||||||
// Une liste de courbes par modèle sélectionné (clé = "ESE" / "TFS")
|
// Une liste de courbes par modèle sélectionné (clé = "ESE" / "TFS")
|
||||||
@ -266,7 +273,13 @@ fun ChartScreen() {
|
|||||||
)
|
)
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = showForecast,
|
selected = showForecast,
|
||||||
onClick = { showForecast = !showForecast },
|
onClick = {
|
||||||
|
showForecast = !showForecast
|
||||||
|
// v1.4.1 : désactiver la prévision pendant qu'on est dans le
|
||||||
|
// futur → revenir à maintenant (sinon fenêtre vide au-delà
|
||||||
|
// de « maintenant » sans doses projetées).
|
||||||
|
if (!showForecast) panHours = maxOf(panHours, 0L)
|
||||||
|
},
|
||||||
enabled = forecastHorizonHours != null,
|
enabled = forecastHorizonHours != null,
|
||||||
label = { Text(stringResource(R.string.show_forecast)) }
|
label = { Text(stringResource(R.string.show_forecast)) }
|
||||||
)
|
)
|
||||||
@ -303,13 +316,16 @@ fun ChartScreen() {
|
|||||||
// Pan (1 doigt) + pinch-zoom (2 doigts, v1.2.9) — un seul
|
// Pan (1 doigt) + pinch-zoom (2 doigts, v1.2.9) — un seul
|
||||||
// handler pour éviter les conflits de consommation
|
// handler pour éviter les conflits de consommation
|
||||||
detectTransformGestures { centroid, pan, zoom, _ ->
|
detectTransformGestures { centroid, pan, zoom, _ ->
|
||||||
val forecastMs =
|
|
||||||
(if (showForecast) (forecastHorizonHours ?: 0L) * HOUR_MS else 0L)
|
|
||||||
if (chartWidthPx > 0) {
|
if (chartWidthPx > 0) {
|
||||||
// Pan horizontal : glisser à droite = remonter le temps
|
// Pan horizontal : glisser à DROITE = remonter le
|
||||||
|
// temps (panHours croît) ; glisser à GAUCHE =
|
||||||
|
// avancer dans le FUTUR (panHours décroît, borné
|
||||||
|
// par l'horizon de prévision — v1.4.1)
|
||||||
val hoursDragged = (pan.x / chartWidthPx * rangeHours).toLong()
|
val hoursDragged = (pan.x / chartWidthPx * rangeHours).toLong()
|
||||||
if (hoursDragged != 0L) {
|
if (hoursDragged != 0L) {
|
||||||
panHours = (panHours + hoursDragged).coerceIn(0L, maxPanHours)
|
panHours = com.hormonetrack.ui.components.clampPanHours(
|
||||||
|
panHours + hoursDragged, futurePanHorizon, maxPanHours
|
||||||
|
)
|
||||||
}
|
}
|
||||||
// Zoom autour du point focal : le temps sous le doigt
|
// Zoom autour du point focal : le temps sous le doigt
|
||||||
// reste fixe pendant le pinch
|
// reste fixe pendant le pinch
|
||||||
@ -318,11 +334,13 @@ fun ChartScreen() {
|
|||||||
val newRange = (rangeHours / zoom).toLong()
|
val newRange = (rangeHours / zoom).toLong()
|
||||||
.coerceIn(MIN_RANGE_H, MAX_RANGE_H)
|
.coerceIn(MIN_RANGE_H, MAX_RANGE_H)
|
||||||
if (newRange != rangeHours) {
|
if (newRange != rangeHours) {
|
||||||
val endAnchor = nowMs + forecastMs - panHours * HOUR_MS
|
val endAnchor = nowMs - panHours * HOUR_MS
|
||||||
val tFocal = endAnchor - ((1.0 - f) * rangeHours * HOUR_MS).toLong()
|
val tFocal = endAnchor - ((1.0 - f) * rangeHours * HOUR_MS).toLong()
|
||||||
val newEnd = tFocal + ((1.0 - f) * newRange * HOUR_MS).toLong()
|
val newEnd = tFocal + ((1.0 - f) * newRange * HOUR_MS).toLong()
|
||||||
val newPan = ((nowMs + forecastMs - newEnd) / HOUR_MS).toLong()
|
val newPan = ((nowMs - newEnd) / HOUR_MS).toLong()
|
||||||
panHours = newPan.coerceIn(0L, maxPanHours)
|
panHours = com.hormonetrack.ui.components.clampPanHours(
|
||||||
|
newPan, futurePanHorizon, maxPanHours
|
||||||
|
)
|
||||||
rangeHours = newRange
|
rangeHours = newRange
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -354,8 +372,10 @@ fun ChartScreen() {
|
|||||||
)
|
)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
||||||
// Retour au présent dès qu'on a voyagé dans le passé
|
// Retour au présent dès qu'on a voyagé (passé OU futur —
|
||||||
if (panHours > 0L) {
|
// v1.4.1 : le futur est atteint en tirant vers la gauche
|
||||||
|
// avec la prévision active)
|
||||||
|
if (panHours != 0L) {
|
||||||
TextButton(onClick = { panHours = 0L }) {
|
TextButton(onClick = { panHours = 0L }) {
|
||||||
Text(stringResource(R.string.back_to_present))
|
Text(stringResource(R.string.back_to_present))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -362,17 +362,31 @@ private fun NextDoseCard(treatments: List<Treatment>, doseLogs: List<DoseLog>) {
|
|||||||
val deltaMs = next - System.currentTimeMillis()
|
val deltaMs = next - System.currentTimeMillis()
|
||||||
val h = deltaMs / HOUR_MS
|
val h = deltaMs / HOUR_MS
|
||||||
val m = (deltaMs % HOUR_MS) / 60_000L
|
val m = (deltaMs % HOUR_MS) / 60_000L
|
||||||
val timeStr = DateTimeFormatter.ofPattern("HH:mm")
|
val name = nextTreatment?.name ?: ""
|
||||||
.withZone(ZoneId.systemDefault())
|
// v1.4.1 : au-delà de 24 h, afficher en JOURS (les heures
|
||||||
.format(Instant.ofEpochMilli(next))
|
// deviennent peu lisibles) + le jour de la semaine dans la date.
|
||||||
Text(
|
if (h >= 24) {
|
||||||
if (h > 0) {
|
val (days, remH) = com.hormonetrack.util.HrtDuration.daysAndHours(h)
|
||||||
String.format(Locale.getDefault(), "%dh%02d · %s (%s)", h, m, timeStr, nextTreatment?.name ?: "")
|
val dayTimeStr = DateTimeFormatter.ofPattern("EEE d HH:mm")
|
||||||
} else {
|
.withZone(ZoneId.systemDefault())
|
||||||
String.format(Locale.getDefault(), "%d min · %s (%s)", m, timeStr, nextTreatment?.name ?: "")
|
.format(Instant.ofEpochMilli(next))
|
||||||
},
|
Text(
|
||||||
style = MaterialTheme.typography.titleMedium
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,4 +12,12 @@ object HrtDuration {
|
|||||||
/** Décomposition mois(30 j) / jours restants. */
|
/** Décomposition mois(30 j) / jours restants. */
|
||||||
fun monthsAndDays(totalDays: Long): Pair<Long, Long> =
|
fun monthsAndDays(totalDays: Long): Pair<Long, Long> =
|
||||||
Pair(totalDays / 30, totalDays % 30)
|
Pair(totalDays / 30, totalDays % 30)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conversion d'un delta EN HEURES en (jours, heures restantes) — v1.4.1,
|
||||||
|
* carte « Prochaine dose » de l'accueil : au-delà de 24 h, les heures
|
||||||
|
* parlent moins que les jours (ex. 120 h → 5 j 0 h).
|
||||||
|
*/
|
||||||
|
fun daysAndHours(totalHours: Long): Pair<Long, Long> =
|
||||||
|
Pair(totalHours / 24, totalHours % 24)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
<string name="no_data">Pas encore de données — crée un traitement et logue une dose.</string>
|
<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="logged_today">Log rapide</string>
|
||||||
<string name="next_dose">Prochaine dose</string>
|
<string name="next_dose">Prochaine dose</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="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>
|
<string name="home_chart_title">Dernières 24 h (estimation)</string>
|
||||||
<string name="home_legend_e2">— E2 estimé (pg/mL, gauche)</string>
|
<string name="home_legend_e2">— E2 estimé (pg/mL, gauche)</string>
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
<string name="no_data">No data yet — add a treatment and log a dose.</string>
|
<string name="no_data">No data yet — add a treatment and log a dose.</string>
|
||||||
<string name="logged_today">Quick log</string>
|
<string name="logged_today">Quick log</string>
|
||||||
<string name="next_dose">Next dose</string>
|
<string name="next_dose">Next dose</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="delta_6h">%1$s %2$s pg/mL vs 6 h ago</string>
|
||||||
<string name="home_chart_title">Last 24 hours (estimate)</string>
|
<string name="home_chart_title">Last 24 hours (estimate)</string>
|
||||||
<string name="home_legend_e2">— E2 estimate (pg/mL, left)</string>
|
<string name="home_legend_e2">— E2 estimate (pg/mL, left)</string>
|
||||||
|
|||||||
@ -1,12 +1,18 @@
|
|||||||
package com.hormonetrack.ui
|
package com.hormonetrack.ui
|
||||||
|
|
||||||
|
import com.hormonetrack.ui.components.clampPanHours
|
||||||
|
import com.hormonetrack.ui.components.forecastHorizonHours
|
||||||
import com.hormonetrack.ui.components.stepForRange
|
import com.hormonetrack.ui.components.stepForRange
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pas d'échantillonnage adaptatif au zoom (v1.2.9) : plus la fenêtre est
|
* Pas d'échantillonnage adaptatif au zoom (v1.2.9) : plus la fenêtre est
|
||||||
* courte, plus le pas est fin — sinon les courbes paraissent anguleuses.
|
* courte, plus le pas est fin — sinon les courbes paraissent anguleuses.
|
||||||
|
*
|
||||||
|
* + v1.4.1 : horizon de prévision (12 × Posologie, borné 30–365 j) et bornes
|
||||||
|
* du panoramique (panHours négatif = futur ; clamp unique drag/zoom).
|
||||||
*/
|
*/
|
||||||
class ChartZoomTest {
|
class ChartZoomTest {
|
||||||
|
|
||||||
@ -28,4 +34,46 @@ class ChartZoomTest {
|
|||||||
assertEquals(3_600_000L, stepForRange(24L * 30))
|
assertEquals(3_600_000L, stepForRange(24L * 30))
|
||||||
assertEquals(3_600_000L, stepForRange(24L * 300))
|
assertEquals(3_600_000L, stepForRange(24L * 300))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `forecast horizon is null without posology`() {
|
||||||
|
assertNull(forecastHorizonHours(null))
|
||||||
|
assertNull(forecastHorizonHours(0.0))
|
||||||
|
assertNull(forecastHorizonHours(-2.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `forecast horizon is 12x the largest interval bounded 30d-365d`() {
|
||||||
|
// Posologie 6 j → 72 j d'horizon (12 × 6)
|
||||||
|
assertEquals(24L * 72, forecastHorizonHours(6.0))
|
||||||
|
// petits intervalles → plancher 30 j (sinon horizon trop court)
|
||||||
|
assertEquals(24L * 30, forecastHorizonHours(1.0))
|
||||||
|
// grands intervalles : 30 j × 12 = 360 j (sous le plafond)…
|
||||||
|
assertEquals(24L * 360, forecastHorizonHours(30.0))
|
||||||
|
// …et plafond 1 an au-delà de ~30,4 j (l'« infini » s'arrête là)
|
||||||
|
assertEquals(24L * 365, forecastHorizonHours(31.0))
|
||||||
|
assertEquals(24L * 365, forecastHorizonHours(90.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `pan clamps to the past only when forecast is off`() {
|
||||||
|
// Prévision OFF : futur interdit (horizon 0) → un drag négatif ramène à 0
|
||||||
|
assertEquals(0L, clampPanHours(panHours = -48L, futureHorizonHours = 0L, maxPastHours = 100L))
|
||||||
|
assertEquals(50L, clampPanHours(50L, 0L, 100L))
|
||||||
|
// bornes passées respectées
|
||||||
|
assertEquals(100L, clampPanHours(500L, 0L, 100L))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `pan can go into the future up to the forecast horizon`() {
|
||||||
|
// Prévision ON (horizon 72 j) : panHours négatif = fenêtre décalée
|
||||||
|
// vers le futur — c'est ainsi qu'on « scrolle à droite » sans saut
|
||||||
|
// à l'activation du chip.
|
||||||
|
assertEquals(-24L * 72, clampPanHours(-24L * 500, 24L * 72, 100L))
|
||||||
|
assertEquals(-5L, clampPanHours(-5L, 24L * 72, 100L))
|
||||||
|
// le passé reste borné par maxPast
|
||||||
|
assertEquals(100L, clampPanHours(1000L, 24L * 72, 100L))
|
||||||
|
// bornes dégénérées (jamais négatives)
|
||||||
|
assertEquals(0L, clampPanHours(-10L, -5L, -3L))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,4 +35,16 @@ class HrtDurationTest {
|
|||||||
fun `décomposition - zéro jour`() {
|
fun `décomposition - zéro jour`() {
|
||||||
assertEquals(Pair(0L, 0L), HrtDuration.monthsAndDays(0L))
|
assertEquals(Pair(0L, 0L), HrtDuration.monthsAndDays(0L))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `heures en jours et heures restantes - carte prochaine dose v1_4_1`() {
|
||||||
|
// 120 h = 5 j 0 h
|
||||||
|
assertEquals(Pair(5L, 0L), HrtDuration.daysAndHours(120L))
|
||||||
|
// 51 h = 2 j 3 h
|
||||||
|
assertEquals(Pair(2L, 3L), HrtDuration.daysAndHours(51L))
|
||||||
|
// < 24 h → 0 j (l'écran utilise alors le format heures/minutes)
|
||||||
|
assertEquals(Pair(0L, 23L), HrtDuration.daysAndHours(23L))
|
||||||
|
// zéro
|
||||||
|
assertEquals(Pair(0L, 0L), HrtDuration.daysAndHours(0L))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,30 @@
|
|||||||
# Changelog — HormoneTrack
|
# Changelog — HormoneTrack
|
||||||
|
|
||||||
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.0`, …).
|
Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.4.1`, …).
|
||||||
|
|
||||||
|
## [1.4.1] — 2026-09-06 (versionCode 21)
|
||||||
|
|
||||||
|
### Ajouté
|
||||||
|
- **Prévision étendue ET sans saut temporel** (remontées utilisateur) :
|
||||||
|
- activer le chip **Prévision** ne déplace PLUS le graphique « tout à
|
||||||
|
droite » dans le futur — la fenêtre reste exactement où elle était ;
|
||||||
|
la courbe projetée s'étend au-delà et on la parcourt en **tirant vers
|
||||||
|
la gauche** (« scroller à l'infini » = jusqu'à 1 an de projection) ;
|
||||||
|
« Revenir à maintenant » ramène depuis le passé **ou** le futur ;
|
||||||
|
- l'horizon passe de « 2 × Posologie, borné 7–30 j » à **12 × Posologie,
|
||||||
|
borné 30 j – 1 an** (`forecastHorizonHours`, épinglé par `ChartZoomTest`).
|
||||||
|
- désactiver la prévision pendant qu'on est dans le futur → retour
|
||||||
|
automatique à « maintenant » (sinon fenêtre sans données).
|
||||||
|
- **Delta en JOURS sur la carte « Prochaine dose »** (remontée : les heures
|
||||||
|
deviennent illisibles au-delà de 24 h) : à partir d'un jour, la carte
|
||||||
|
affiche « 5 j 2 h · sam. 6 18:00 (traitement) » avec le jour de la
|
||||||
|
semaine ; en dessous, format heures/minutes inchangé
|
||||||
|
(`HrtDuration.daysAndHours` + string `next_dose_days` FR/EN).
|
||||||
|
- **5 nouveaux tests** (112 au total / 94 sans les données locales) :
|
||||||
|
bornes d'horizon et clamp de panoramique (`ChartZoomTest` ×4 — le clamp
|
||||||
|
unique drag/zoom est centralisé dans `clampPanHours`),
|
||||||
|
conversion heures→jours (`HrtDurationTest` ×1).
|
||||||
|
|
||||||
## [1.4.0] — 2026-09-06 (versionCode 20)
|
## [1.4.0] — 2026-09-06 (versionCode 20)
|
||||||
|
|
||||||
|
|||||||
@ -83,6 +83,7 @@ Fonctionnalités v1 :
|
|||||||
| 6 sept. 2026 (session v1.3.4) | **Deux crashs v1.3.3 remontés (Doses + export logs)** → reproduits sur ÉMULATEUR avec les vraies données (recette §16.ter, désormais standard) : (a) Doses = string `hrt_duration` 3 placeholders vs 2 args (**né en v1.3.1**, §14 #47) ; (b) export logs = `LocalDate.format("…-HHmm")` levant `UnsupportedTemporalTypeException` au TAP (§14 #48) — les fixes IO v1.3.2/v1.3.3 étaient à côté du vrai problème. Lint mis en filet bloquant (aurait attrapé les deux) + #49 notif ; helper `ExportFileNames` + 3 tests (90 verts) ; fixes VÉRIFIÉS sur émulateur avec l'APK release + données réelles (flux SAF complet : SAVE → fichier + message). Publication v1.3.4. |
|
| 6 sept. 2026 (session v1.3.4) | **Deux crashs v1.3.3 remontés (Doses + export logs)** → reproduits sur ÉMULATEUR avec les vraies données (recette §16.ter, désormais standard) : (a) Doses = string `hrt_duration` 3 placeholders vs 2 args (**né en v1.3.1**, §14 #47) ; (b) export logs = `LocalDate.format("…-HHmm")` levant `UnsupportedTemporalTypeException` au TAP (§14 #48) — les fixes IO v1.3.2/v1.3.3 étaient à côté du vrai problème. Lint mis en filet bloquant (aurait attrapé les deux) + #49 notif ; helper `ExportFileNames` + 3 tests (90 verts) ; fixes VÉRIFIÉS sur émulateur avec l'APK release + données réelles (flux SAF complet : SAVE → fichier + message). Publication v1.3.4. |
|
||||||
| 6 sept. 2026 (session v1.3.5) | **Premier diagnostic à distance RÉUSSI via les logs exportés** : les AppLog de la v1.3.4 prouvent export logs `ok=true` (fix #48 ✓ au téléphone), permission agenda accordée (fix manifest ✓) et révèlent le bug racine EXPLICITE : `ensureCalendar` plantait car l'URI sync-adapter n'embarquait pas ACCOUNT_NAME/ACCOUNT_TYPE (§14 #50). En validant sur émulateur, DEUX bugs découverts : delete d'event sans account (#50 bis — « supprimé » logué mais l'event restait) et `buildTreatment` sans `calendarEventId` (#51 — événements orphelins). Cycle ON→save→OFF→save validé end-to-end (`content query`). Publication v1.3.5. |
|
| 6 sept. 2026 (session v1.3.5) | **Premier diagnostic à distance RÉUSSI via les logs exportés** : les AppLog de la v1.3.4 prouvent export logs `ok=true` (fix #48 ✓ au téléphone), permission agenda accordée (fix manifest ✓) et révèlent le bug racine EXPLICITE : `ensureCalendar` plantait car l'URI sync-adapter n'embarquait pas ACCOUNT_NAME/ACCOUNT_TYPE (§14 #50). En validant sur émulateur, DEUX bugs découverts : delete d'event sans account (#50 bis — « supprimé » logué mais l'event restait) et `buildTreatment` sans `calendarEventId` (#51 — événements orphelins). Cycle ON→save→OFF→save validé end-to-end (`content query`). Publication v1.3.5. |
|
||||||
| 6 sept. 2026 (session v1.4.0) | **Modèle TFS reconstruit sur la méta-analyse officielle** (params V3C du simulateur transfemscience.github.io/injectable-e2-simulator, extraits de `ester-data.js`/`calc-curve.js`) : forme close 3 compartiments, 7 esters (EB/EC/ECS/PEP ajoutés), pics de l'article reproduits à ~1 % (AUC + Figure 11 épinglés). **+ Fix #52** (remontée) : rappels quotidiens même hors jour d'injection → grille Posologie (moteur = source unique), re-programmation après notif et « Pris ». 12 nouveaux tests (102 verts), lint vert. Publication v1.4.0. |
|
| 6 sept. 2026 (session v1.4.0) | **Modèle TFS reconstruit sur la méta-analyse officielle** (params V3C du simulateur transfemscience.github.io/injectable-e2-simulator, extraits de `ester-data.js`/`calc-curve.js`) : forme close 3 compartiments, 7 esters (EB/EC/ECS/PEP ajoutés), pics de l'article reproduits à ~1 % (AUC + Figure 11 épinglés). **+ Fix #52** (remontée) : rappels quotidiens même hors jour d'injection → grille Posologie (moteur = source unique), re-programmation après notif et « Pris ». 12 nouveaux tests (102 verts), lint vert. Publication v1.4.0. |
|
||||||
|
| 6 sept. 2026 (session v1.4.1) | **UX graphique et carte « Prochaine dose »** (remontées) : (a) activer la Prévision ne saute PLUS dans le futur — la fenêtre reste en place, la projection s'étend au-delà et se parcourt en tirant vers la gauche (panHours NÉGATIF = futur, clamp unique `clampPanHours`, horizon 12× Posologie borné 30 j–1 an) ; (b) delta en JOURS sur la carte « Prochaine dose » au-delà de 24 h (`HrtDuration.daysAndHours` + `next_dose_days` FR/EN). 5 tests (112 verts). Publication v1.4.1. |
|
||||||
| 6 sept. 2026 (session v1.3.0) | Dialog « Nouveautés » après mise à jour (CHANGELOG embarqué en asset, tâche `copyChangelog`, version vue en DataStore) ; **événements d'agenda récurrents** (calendrier local HormoneTrack, RRULE posologie, permission runtime, Room v3 `calendarEventId`) ; Paramètres : version + lien releases ; **78 tests verts**, APK v1.3.0 + releases. |
|
| 6 sept. 2026 (session v1.3.0) | Dialog « Nouveautés » après mise à jour (CHANGELOG embarqué en asset, tâche `copyChangelog`, version vue en DataStore) ; **événements d'agenda récurrents** (calendrier local HormoneTrack, RRULE posologie, permission runtime, Room v3 `calendarEventId`) ; Paramètres : version + lien releases ; **78 tests verts**, APK v1.3.0 + releases. |
|
||||||
| 5 sept. 2026 (session v1.2.10) | Sens des boutons de zoom inversé (+ = zoom avant, convention carte — retour utilisateur) ; 67 tests verts, APK v1.2.10 + releases. |
|
| 5 sept. 2026 (session v1.2.10) | Sens des boutons de zoom inversé (+ = zoom avant, convention carte — retour utilisateur) ; 67 tests verts, APK v1.2.10 + releases. |
|
||||||
| 5 sept. 2026 (session v1.2.9) | **Zoom du graphique** (pinch + boutons, 6 h → 300 j, focal stable, échantillonnage adaptatif `stepForRange`, labels X 1 h/3 h) ; **README : disclaimer IA-assisté** en en-tête ; 3 tests ; **67 tests verts**, APK v1.2.9 + releases. |
|
| 5 sept. 2026 (session v1.2.9) | **Zoom du graphique** (pinch + boutons, 6 h → 300 j, focal stable, échantillonnage adaptatif `stepForRange`, labels X 1 h/3 h) ; **README : disclaimer IA-assisté** en en-tête ; 3 tests ; **67 tests verts**, APK v1.2.9 + releases. |
|
||||||
@ -407,7 +408,7 @@ Cp(t) = D·k1·k2 · [ e^(−k1·t)/((k1−k2)(k1−k3)) + e^(−k3·t)/((k1−k
|
|||||||
|
|
||||||
## 8. Tests unitaires
|
## 8. Tests unitaires
|
||||||
|
|
||||||
**107 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — **89 sans
|
**112 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — **94 sans
|
||||||
les données de test locales** (cf §8.bis : les 3 classes de régression,
|
les données de test locales** (cf §8.bis : les 3 classes de régression,
|
||||||
6 tests chacune, sont skippées via `Assume`). Dépendance : JUnit 4.13.2.
|
6 tests chacune, 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 =
|
Emplacement : `app/src/test/java/com/hormonetrack/`. Répertoire de travail d'exécution =
|
||||||
@ -503,7 +504,7 @@ dépôt (ni maintenant, ni si le repo devient public un jour).
|
|||||||
seule), plusieurs versions intermédiaires, versions non numériques.
|
seule), plusieurs versions intermédiaires, versions non numériques.
|
||||||
- **`AppLogTest`** (4) : v1.3.1 — formatage des lignes horodatées, buffer
|
- **`AppLogTest`** (4) : v1.3.1 — formatage des lignes horodatées, buffer
|
||||||
circulaire (600 → 500 dernières, bordures).
|
circulaire (600 → 500 dernières, bordures).
|
||||||
- **`HrtDurationTest`** (5) : v1.3.1 — jours depuis la 1re prise (0 si
|
- **`HrtDurationTest`** (6) : v1.3.1 — jours depuis la 1re prise (0 si
|
||||||
futur/invalide), décomposition mois(30 j)/jours.
|
futur/invalide), décomposition mois(30 j)/jours.
|
||||||
- **`CalendarRruleTest`** (3) : v1.3.0 — RRULE `FREQ=DAILY;INTERVAL=N`,
|
- **`CalendarRruleTest`** (3) : v1.3.0 — RRULE `FREQ=DAILY;INTERVAL=N`,
|
||||||
arrondi demi-supérieur des décimales (6,5 → 7), clamp ≥ 1 jour.
|
arrondi demi-supérieur des décimales (6,5 → 7), clamp ≥ 1 jour.
|
||||||
@ -526,6 +527,7 @@ dépôt (ni maintenant, ni si le repo devient public un jour).
|
|||||||
et 0 au-delà de 10 × t½ (par design) ; `computeCurve(modelOverride=TFS)`
|
et 0 au-delà de 10 × t½ (par design) ; `computeCurve(modelOverride=TFS)`
|
||||||
sur un traitement stocké ESE reproduit le pic V3C EV 295 — c'est le test
|
sur un traitement stocké ESE reproduit le pic V3C EV 295 — c'est le test
|
||||||
du dispatch `concentrationOfDose` + de `cutoffHours`.
|
du dispatch `concentrationOfDose` + de `cutoffHours`.
|
||||||
|
- **`ChartZoomTest`** (7) : v1.2.9 (pas d'échantillonnage) + v1.4.1 — **horizon de prévision** 12 × Posologie borné 30 j–1 an (`forecastHorizonHours`, null sans Posologie) et **clamp du panoramique** (`clampPanHours` : panHours négatif = futur, borné par l'horizon ; prévision OFF → futur interdit) — la logique de fenêtre du graphique est centralisée et testée.
|
||||||
- **`ReminderScheduleTest`** (8) : v1.4.0 — fix #52 : rappel sur la GRILLE
|
- **`ReminderScheduleTest`** (8) : v1.4.0 — fix #52 : rappel sur la GRILLE
|
||||||
Posologie (dose il y a 2 j + intervalle 7 j → alarme dans 5 j à l'heure
|
Posologie (dose il y a 2 j + intervalle 7 j → alarme dans 5 j à l'heure
|
||||||
choisie, PAS demain), heure déjà passée le jour du créneau → créneau
|
choisie, PAS demain), heure déjà passée le jour du créneau → créneau
|
||||||
@ -590,11 +592,20 @@ Sur la montre : remontée par Gadgetbridge **ou** Huawei Health (cf §17).
|
|||||||
- `HormoneTrackRoot` : NavigationBar 5 tabs (home/chart/doses/labs/treatments) + routes
|
- `HormoneTrackRoot` : NavigationBar 5 tabs (home/chart/doses/labs/treatments) + routes
|
||||||
`settings`, `treatment_edit/{id}` (-1 = nouveau) ; barre masquée sur ces 2 routes
|
`settings`, `treatment_edit/{id}` (-1 = nouveau) ; barre masquée sur ces 2 routes
|
||||||
- `HomeScreen` : bandeau gradient (TransSky→TransPink, discret), carte **niveau actuel**
|
- `HomeScreen` : bandeau gradient (TransSky→TransPink, discret), carte **niveau actuel**
|
||||||
(E2 ≈ X pg/mL, T ≈ Y ng/mL, delta vs 6 h), carte prochaine dose, chips de log rapide
|
(E2 ≈ X pg/mL, T ≈ Y ng/mL, delta vs 6 h), carte prochaine dose (**v1.4.1 :
|
||||||
|
delta en JOURS au-delà de 24 h — « 5 j 2 h · sam. 6 18:00 » via
|
||||||
|
`HrtDuration.daysAndHours` + `next_dose_days` ; en dessous, h/min**), chips de log rapide
|
||||||
(+ FAB), mini-chart 24 h (multi-séries via `ChartSeries`) **cliquable → écran
|
(+ FAB), mini-chart 24 h (multi-séries via `ChartSeries`) **cliquable → écran
|
||||||
Graphiques** (v1.2.1) avec mini-légende E2/T ; données auto-calibrées si l'option est
|
Graphiques** (v1.2.1) avec mini-légende E2/T ; données auto-calibrées si l'option est
|
||||||
active (`scalePerEster = effectiveAuto?.esterScales`) ; rafraîchissement `tick` 60 s
|
active (`scalePerEster = effectiveAuto?.esterScales`) ; rafraîchissement `tick` 60 s
|
||||||
- `ChartScreen` (v1.2, le plus riche) : plages 24 h/7 j/30 j + **boutons zoom − / +**
|
- `ChartScreen` (v1.2, le plus riche) : **fenêtre temporelle v1.4.1** —
|
||||||
|
`endMs = now − panHours` avec `panHours` signé : > 0 = passé (tirer vers la
|
||||||
|
droite), **< 0 = futur** (tirer vers la gauche, uniquement avec la
|
||||||
|
prévision active, borné par `forecastHorizonHours` = 12 × Posologie borné
|
||||||
|
30 j–1 an). **Activer le chip Prévision ne déplace PAS la fenêtre** (la
|
||||||
|
projection s'étend au-delà et se parcourt au drag) ; désactiver pendant un
|
||||||
|
voyage futur → retour à maintenant ; clamp centralisé dans
|
||||||
|
`clampPanHours` (testé). Plages 24 h/7 j/30 j + **boutons zoom − / +**
|
||||||
(v1.2.9) ; **panoramique**
|
(v1.2.9) ; **panoramique**
|
||||||
(`detectHorizontalDragGestures` — tirer vers la droite remonte dans le passé,
|
(`detectHorizontalDragGestures` — tirer vers la droite remonte dans le passé,
|
||||||
`panHours` borné à [0, âge de la 1ʳᵉ dose + plage], bouton « Revenir à maintenant ») ;
|
`panHours` borné à [0, âge de la 1ʳᵉ dose + plage], bouton « Revenir à maintenant ») ;
|
||||||
@ -1147,8 +1158,8 @@ vérifie pas en tests JVM).
|
|||||||
|
|
||||||
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` — 107 au
|
2. **Tests verts obligatoires** : `./gradlew testDebugUnitTest` — 112 au
|
||||||
total, 89 si `local-test-data/` est absent (les 3 classes de régression
|
total, 94 si `local-test-data/` est absent (les 3 classes de régression
|
||||||
réelles sont skippées via `Assume`, 6 tests chacune) ; **lint vert
|
réelles sont skippées via `Assume`, 6 tests chacune) ; **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). Paliers de test du projet :
|
attrapé les crashs #47/#48 dès v1.3.1). Paliers de test du projet :
|
||||||
@ -1418,14 +1429,22 @@ Sur le téléphone de test (à compléter par l'utilisatrice) :
|
|||||||
pg/mL/mg) et diverger en queues (V3C = queues exactes) ; vérifier un
|
pg/mL/mg) et diverger en queues (V3C = queues exactes) ; vérifier un
|
||||||
preset EB/EC/PEP (nouvelles courbes) ; PEP à 32,5 mg → niveaux
|
preset EB/EC/PEP (nouvelles courbes) ; PEP à 32,5 mg → niveaux
|
||||||
comparables à un EEn à 5 mg
|
comparables à un EEn à 5 mg
|
||||||
|
- [ ] **v1.4.1** : **prévision sans saut** : ouvrir Graphiques → activer le
|
||||||
|
chip Prévision → le graphique NE BOUGE PAS (avant v1.4.1 il sautait
|
||||||
|
« tout à droite ») ; tirer vers la GAUCHE → la courbe projetée défile
|
||||||
|
vers le futur (jusqu'à 1 an selon la Posologie) ; « Revenir à
|
||||||
|
maintenant » ramène ; désactiver le chip dans le futur → retour auto
|
||||||
|
- [ ] **v1.4.1** : **carte « Prochaine dose » en jours** : avec un créneau
|
||||||
|
à > 24 h, l'accueil affiche « 5 j 2 h · sam. 6 18:00 (traitement) »
|
||||||
|
(et non « 122h22 ») ; à < 24 h, format heures/minutes inchangé
|
||||||
|
|
||||||
---
|
---
|
||||||
*Doc mise à jour le 6 sept. 2026 (v1.4.0) — build OK, lint vert, 107/107 tests verts (89 sans
|
*Doc mise à jour le 6 sept. 2026 (v1.4.1) — build OK, lint vert, 112/112 tests verts (94 sans
|
||||||
les données locales), dépôts Gitea (cloudyfy + farewell) avec releases APK, aucune donnée
|
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
|
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
|
maintenance (permissions agenda, export logs IO, bump de version) ; v1.3.4 = 2 crashs
|
||||||
reproduits sur émulateur (#47 stringResource arity, #48 LocalDate+pattern horaire), lint
|
reproduits sur émulateur (#47 stringResource arity, #48 LocalDate+pattern horaire), lint
|
||||||
filet bloquant ; v1.3.5 = diagnostic à distance via les logs exportés (#50/#50bis/#51
|
filet bloquant ; v1.3.5 = diagnostic à distance via les logs exportés (#50/#50bis/#51
|
||||||
agenda) ; v1.4.0 = modèle TFS V3C sur la méta-analyse officielle (7 esters, fidélité
|
agenda) ; v1.4.0 = modèle TFS V3C sur la méta-analyse officielle (7 esters, fidélité
|
||||||
~1 % épinglée) + rappels sur la grille Posologie (#52). Dette connue : fragments de
|
~1 % épinglée) + rappels sur la grille Posologie (#52) ; v1.4.1 = prévision étendue sans saut (scroll futur, horizon 1 an) + delta en jours sur l'accueil. Dette connue : fragments de
|
||||||
labs dans l'historique git (v1.1.0→v1.2.3) — cf §8.bis.*
|
labs dans l'historique git (v1.1.0→v1.2.3) — cf §8.bis.*
|
||||||
|
|||||||
@ -101,6 +101,8 @@ 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`
|
- 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)
|
- Pour gel/patch/oral : paramètres Bateman (temps au pic, demi-vie, biodispo)
|
||||||
2. **Doses → +** → logue tes injections passées
|
2. **Doses → +** → logue tes injections passées
|
||||||
|
- L'accueil affiche la **prochaine dose en jours** quand elle est à plus de 24 h
|
||||||
|
(ex. « 5 j 2 h · sam. 6 18:00 »), en heures/minutes sinon
|
||||||
- Astuce mise à jour : l'app affiche les **nouveautés** de chaque version au
|
- Astuce mise à jour : l'app affiche les **nouveautés** de chaque version au
|
||||||
démarrage (fermable) ; la **version installée** et le **lien des releases**
|
démarrage (fermable) ; la **version installée** et le **lien des releases**
|
||||||
sont dans Paramètres (date/heure exactes, dose en mg)
|
sont dans Paramètres (date/heure exactes, dose en mg)
|
||||||
@ -164,7 +166,11 @@ Pics de référence (dose unique de 5 mg, IM) :
|
|||||||
simultanément (Estrannaise = bleu, Transfem Science = turquoise) pour comparer
|
simultanément (Estrannaise = bleu, Transfem Science = turquoise) pour comparer
|
||||||
- **Prévision** : configure la **Posologie** (intervalle en jours) dans un traitement
|
- **Prévision** : configure la **Posologie** (intervalle en jours) dans un traitement
|
||||||
(section « Fréquence ») puis active le chip « Prévision » → les doses à venir sont
|
(section « Fréquence ») puis active le chip « Prévision » → les doses à venir sont
|
||||||
simulées et dessinées après la ligne « maintenant » (jamais sauvegardées)
|
simulées et dessinées après la ligne « maintenant » (jamais sauvegardées).
|
||||||
|
**Activer le chip ne déplace pas le graphique** : tire vers la GAUCHE pour parcourir
|
||||||
|
la projection dans le futur (jusqu'à 1 an selon la Posologie) ; « Revenir à
|
||||||
|
maintenant » pour revenir. Si tu désactives le chip alors que tu es dans le futur,
|
||||||
|
l'app revient automatiquement à maintenant
|
||||||
- **Pics / creux** (chip sur le graphique) : triangles ▲▼ aux extrema estimés de
|
- **Pics / creux** (chip sur le graphique) : triangles ▲▼ aux extrema estimés de
|
||||||
chaque courbe (E2 et T, les deux modèles) — pratique pour visualiser d'un coup
|
chaque courbe (E2 et T, les deux modèles) — pratique pour visualiser d'un coup
|
||||||
d'œil les hauts et les bas entre deux injections
|
d'œil les hauts et les bas entre deux injections
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user