v1.8.2 : maintenance — perf moteur, fraîcheur UI, code/doc nettoyés
Session d'audit complet (code + doc, 46 fichiers + 4 docs) et application des constats sûrs. PERF MOTEUR PK (résultat identique — ordre de sommation préservé, 223 tests verts inchangés) : - e2At/computeCurve : cutoffHours pré-calculé par traitement (le chemin chaud du tracé labs recalculait ~430 k appels/refresh), Bateman paresseux (inutile pour les traitements à profil), doses pré-groupées par traitement (fin des scans complets). - AppLog : purge sans recopie ; ChangelogHelper : Regex précompilées ; CurveChart : SimpleDateFormat hors boucle de dessin. FIX FRAÎCHEUR (#65/#66) : - Accueil : clés produceState complétées (allTreatments, tConfig) — éditer un scaleFactor/le modèle T actualise la courbe sans attendre le tick de 60 s. - Graphiques : clé labResults (liste entière, plus .size) — éditer la valeur d'une analyse actualise les courbes. MAINTENANCE : - Code mort supprimé (Repository/DAO sans appelants, currentLevel, scheduler2, variable morte, Spacer no-op) ; imports morts/redondants nettoyés (9 fichiers) ; qualifications raccourcies. - Médiane de calibration factorisée (4 copies → medianRounded) ; double-filtre computeTConfigCalibration supprimé (soussumé) ; formatDose dédupliqué ; en-têtes/KDoc obsolètes mis à jour. DOC : ~35 corrections (compteurs actuels 223/193 partout — §16, §14, §8.bis ; TOC + 4 ancres réparées ; inventaire §8 à jour + fusions ChartZoom/WhsahEngine + ajout LabTiming/AutoBackupRetention/ ExportFileNames ; titres CHANGELOG doublés [1.4.9]/[1.4.8] supprimés ; §19 backup = manuel + auto ; versions « v1.2.0 »/« Gradle 8.9 »/« API 34 » corrigées ; ratios #61 complétés 383/391 ; §21 item v1.8.1 + chronologie ; §14 #65/#66 ajoutés). Rétention Ratios (#14 #61) : vérifié via ScaleFactorWhsahReproTest — les 5 valeurs 3,3/15,9/89,6/383/391 existent. Validé émulateur §16.ter (release, seed réel v1.7.0) : accueil (encart reco), Analyses (creux 27/09 + notes), Graphiques, 0 crash. versionCode 37 / versionName 1.8.2. Web non concerné (aucun changement).
This commit is contained in:
parent
2ec9f63f93
commit
f6be3d86a9
@ -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.8.1 — build Android ✅, **lint vert** ✅, **223 tests unitaires** ✅ (193 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.8.2 — build Android ✅, **lint vert** ✅, **223 tests unitaires** ✅ (193 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)
|
||||
@ -64,11 +64,11 @@ sauvegarde JSON. **100 % local, aucun compte, aucun serveur.**
|
||||
- **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
|
||||
l'entrée) ; unités T : ng/mL, ng/dL, ng/L, nmol/L
|
||||
- **Prochaine prise de sang suggérée (v1.8.0)** : la page Analyses recommande le
|
||||
- **Prochaine prise de sang suggérée (v1.8.0, étendu v1.8.1 aux pages Analyses et Accueil)** : la page Analyses recommande le
|
||||
**creux estimé juste avant l'injection suivante** (moment le plus comparable),
|
||||
**au premier creux où ton régime est stabilisé** (~5 demi-vies après ton dernier
|
||||
changement — dose, ester ou intervalle) — déduit des courbes et de la Posologie ; requiert un
|
||||
injectable E2 actif à Posologie (une invite invite à la renseigner sinon) ;
|
||||
injectable E2 actif à Posologie (une invite propose de la renseigner sinon) ;
|
||||
estimations, jamais un avis médical
|
||||
- **Calibration** : facteur d'échelle par traitement = médiane(lab ÷ prédiction du modèle),
|
||||
calculé automatiquement (« Scale factor » du `.ods`, automatisé) — ou **calibration
|
||||
@ -128,7 +128,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) —
|
||||
`Siphonight/HormoneTrack` sur les deux (privé), avec
|
||||
**releases taguées** (`v1.1.0` → `v1.4.10`) et **deux APK par release** (depuis v1.2.5)
|
||||
**releases taguées** (`v1.1.0` → `v1.8.1`) et **deux APK par release** (depuis v1.2.5)
|
||||
(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) :
|
||||
|
||||
|
||||
@ -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 = 36
|
||||
versionName = "1.8.1"
|
||||
versionCode = 37
|
||||
versionName = "1.8.2"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
@ -9,6 +9,7 @@ import com.hormonetrack.reminder.createNotificationChannel
|
||||
import com.hormonetrack.util.AppLog
|
||||
import com.hormonetrack.reminder.AlertNotifier
|
||||
import com.hormonetrack.settings.AppSettings
|
||||
import com.hormonetrack.data.backup.AutoBackupScheduler
|
||||
|
||||
class HormoneTrackApp : Application() {
|
||||
|
||||
@ -31,7 +32,7 @@ class HormoneTrackApp : Application() {
|
||||
// le worker sort immédiatement si l'option est désactivée ; cf
|
||||
// data/backup/AutoBackupWorker.kt). WorkManager survit aux reboots :
|
||||
// BootReceiver n'a rien à faire.
|
||||
com.hormonetrack.data.backup.AutoBackupScheduler.schedulePeriodic(this)
|
||||
AutoBackupScheduler.schedulePeriodic(this)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -8,7 +8,6 @@ import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import com.hormonetrack.data.backup.UserSettings
|
||||
import com.hormonetrack.util.AppLog
|
||||
import kotlinx.coroutines.flow.first
|
||||
import java.util.concurrent.TimeUnit
|
||||
@ -109,13 +108,15 @@ class AutoBackupWorker(
|
||||
val ok = try {
|
||||
// MÊME contenu que l'export manuel (format v2 avec paramètres) :
|
||||
// un auto-backup est importable tel quel (rétrocompat assurée)
|
||||
// UNE seule collecte des seuils (même motif que l'export manuel)
|
||||
val thresholds = settings.alertThresholds.first()
|
||||
val userSettings = UserSettings(
|
||||
language = settings.language.first(),
|
||||
autoCalibrate = settings.autoCalibrate.first(),
|
||||
alertE2High = settings.alertThresholds.first().e2High,
|
||||
alertE2Low = settings.alertThresholds.first().e2Low,
|
||||
alertTHigh = settings.alertThresholds.first().tHigh,
|
||||
alertTLow = settings.alertThresholds.first().tLow
|
||||
alertE2High = thresholds.e2High,
|
||||
alertE2Low = thresholds.e2Low,
|
||||
alertTHigh = thresholds.tHigh,
|
||||
alertTLow = thresholds.tLow
|
||||
)
|
||||
val json = BackupManager.exportJson(container.repository, settings.tConfig.first(), userSettings)
|
||||
val keep = settings.autoBackupKeep.first()
|
||||
|
||||
@ -9,17 +9,9 @@ interface DoseLogDao {
|
||||
@Query("SELECT * FROM dose_logs ORDER BY timestamp DESC")
|
||||
fun getAll(): Flow<List<DoseLog>>
|
||||
|
||||
@Query("SELECT * FROM dose_logs WHERE treatmentId = :treatmentId ORDER BY timestamp DESC")
|
||||
fun getByTreatment(treatmentId: Long): Flow<List<DoseLog>>
|
||||
|
||||
@Query("SELECT * FROM dose_logs WHERE timestamp BETWEEN :start AND :end ORDER BY timestamp ASC")
|
||||
fun getByTimeRange(start: Long, end: Long): Flow<List<DoseLog>>
|
||||
|
||||
@Query("SELECT * FROM dose_logs WHERE treatmentId = :treatmentId AND timestamp BETWEEN :start AND :end ORDER BY timestamp ASC")
|
||||
fun getByTreatmentAndTimeRange(treatmentId: Long, start: Long, end: Long): Flow<List<DoseLog>>
|
||||
|
||||
@Query("SELECT * FROM dose_logs ORDER BY timestamp DESC LIMIT 1")
|
||||
fun getLatest(): Flow<DoseLog?>
|
||||
|
||||
@Query("SELECT * FROM dose_logs ORDER BY timestamp ASC")
|
||||
suspend fun getAllOnce(): List<DoseLog>
|
||||
|
||||
@ -9,11 +9,7 @@ interface LabResultDao {
|
||||
@Query("SELECT * FROM lab_results ORDER BY timestamp DESC")
|
||||
fun getAll(): Flow<List<LabResult>>
|
||||
|
||||
@Query("SELECT * FROM lab_results WHERE marker = :marker ORDER BY timestamp DESC")
|
||||
fun getByMarker(marker: String): Flow<List<LabResult>>
|
||||
|
||||
@Query("SELECT * FROM lab_results WHERE timestamp BETWEEN :start AND :end ORDER BY timestamp ASC")
|
||||
fun getByTimeRange(start: Long, end: Long): Flow<List<LabResult>>
|
||||
|
||||
@Query("SELECT * FROM lab_results ORDER BY timestamp ASC")
|
||||
suspend fun getAllOnce(): List<LabResult>
|
||||
|
||||
@ -15,9 +15,6 @@ interface TreatmentDao {
|
||||
@Query("SELECT * FROM treatments WHERE id = :id")
|
||||
suspend fun getById(id: Long): Treatment?
|
||||
|
||||
@Query("SELECT * FROM treatments WHERE isActive = 1 ORDER BY createdAt DESC")
|
||||
suspend fun getActiveOnce(): List<Treatment>
|
||||
|
||||
@Query("SELECT * FROM treatments ORDER BY createdAt DESC")
|
||||
suspend fun getAllOnce(): List<Treatment>
|
||||
|
||||
@ -35,6 +32,4 @@ interface TreatmentDao {
|
||||
@Delete
|
||||
suspend fun delete(treatment: Treatment)
|
||||
|
||||
@Query("UPDATE treatments SET isActive = :active WHERE id = :id")
|
||||
suspend fun setActive(id: Long, active: Boolean)
|
||||
}
|
||||
|
||||
@ -115,6 +115,9 @@ data class Treatment(
|
||||
) {
|
||||
val isInjection: Boolean
|
||||
get() = route == AdministrationRoute.INJECTION_IM || route == AdministrationRoute.INJECTION_SUBCUT
|
||||
// (redondant avec PharmacokineticEngine.isInjectionRoute — volontaire :
|
||||
// data.model ne doit PAS dépendre de pk (cycle de couches). Si la
|
||||
// définition d'« injectable » change, changer les DEUX endroits.)
|
||||
|
||||
val usesProfileModel: Boolean
|
||||
get() = isInjection && esterType != Esters.NONE
|
||||
|
||||
@ -18,15 +18,6 @@ class HormoneRepository(
|
||||
val allDoseLogs: Flow<List<DoseLog>> = doseLogDao.getAll()
|
||||
val allLabResults: Flow<List<LabResult>> = labResultDao.getAll()
|
||||
|
||||
fun getDosesByTimeRange(start: Long, end: Long): Flow<List<DoseLog>> =
|
||||
doseLogDao.getByTimeRange(start, end)
|
||||
|
||||
fun getDosesByTreatmentAndTimeRange(treatmentId: Long, start: Long, end: Long): Flow<List<DoseLog>> =
|
||||
doseLogDao.getByTreatmentAndTimeRange(treatmentId, start, end)
|
||||
|
||||
fun getLabsByMarker(marker: String): Flow<List<LabResult>> =
|
||||
labResultDao.getByMarker(marker)
|
||||
|
||||
suspend fun getTreatmentById(id: Long): Treatment? = treatmentDao.getById(id)
|
||||
|
||||
suspend fun allTreatmentsOnce(): List<Treatment> = treatmentDao.getAllOnce()
|
||||
|
||||
@ -120,9 +120,9 @@ object LabTiming {
|
||||
// ── 2) t½ terminale de l'ester (analytique ou lue dans la table) ────
|
||||
val tHalfDays = when (carrier.pkModel) {
|
||||
PKModels.TRANSFEM_SCIENCE ->
|
||||
com.hormonetrack.pk.TransfemScienceModels.model(carrier.esterType)?.terminalHalfLifeDays
|
||||
TransfemScienceModels.model(carrier.esterType)?.terminalHalfLifeDays
|
||||
PKModels.WHSAH ->
|
||||
com.hormonetrack.pk.WhsahModels.model(carrier.esterType)?.terminalHalfLifeDays
|
||||
WhsahModels.model(carrier.esterType)?.terminalHalfLifeDays
|
||||
PKModels.ESTRANNAISE ->
|
||||
PKProfileStore.terminalHalfLifeDays(carrier.esterType, carrier.pkModel)
|
||||
else -> null
|
||||
|
||||
@ -2,7 +2,6 @@ package com.hormonetrack.pk
|
||||
|
||||
import com.hormonetrack.data.model.DoseLog
|
||||
import com.hormonetrack.data.model.LabResult
|
||||
import com.hormonetrack.data.model.PKModels
|
||||
import com.hormonetrack.data.model.Treatment
|
||||
import com.hormonetrack.data.model.TreatmentType
|
||||
import kotlin.math.exp
|
||||
@ -188,7 +187,7 @@ object LabTrajectoryModel {
|
||||
var maxPredictionSoFar = 0.0
|
||||
for (lab in e2Labs.filter { it.marker.equals("E2", ignoreCase = true) }.sortedBy { it.timestamp }) {
|
||||
val predicted = PharmacokineticEngine.e2At(
|
||||
unscaled, doseLogs, lab.timestamp, modelOverride, null
|
||||
unscaled, doseLogs, lab.timestamp, modelOverride, scalePerEster = null
|
||||
)
|
||||
if (!PharmacokineticEngine.labIsSignificant(predicted, maxPredictionSoFar)) continue
|
||||
anchors.add(lab.timestamp to (lab.value / predicted))
|
||||
@ -221,7 +220,7 @@ object LabTrajectoryModel {
|
||||
val points = mutableListOf<LevelPoint>()
|
||||
var t = t0
|
||||
while (t <= t1) {
|
||||
val m = PharmacokineticEngine.e2At(unscaled, doseLogs, t, modelOverride, null)
|
||||
val m = PharmacokineticEngine.e2At(unscaled, doseLogs, t, modelOverride, scalePerEster = null)
|
||||
val rho = ratioAt(anchors, t)
|
||||
points.add(LevelPoint(t, m * rho, 0.0)) // t=0 : pas de courbe T pour cette série
|
||||
t += stepMs
|
||||
|
||||
@ -2,6 +2,8 @@ package com.hormonetrack.pk
|
||||
|
||||
import android.content.Context
|
||||
import com.google.gson.JsonParser
|
||||
import com.hormonetrack.data.model.PKModels
|
||||
import kotlin.math.ln
|
||||
|
||||
object PKProfileStore {
|
||||
|
||||
@ -36,7 +38,7 @@ object PKProfileStore {
|
||||
}
|
||||
|
||||
fun profileKey(ester: String, model: String): String =
|
||||
"${ester}_${if (model == com.hormonetrack.data.model.PKModels.TRANSFEM_SCIENCE) "tfs" else "ese"}"
|
||||
"${ester}_${if (model == PKModels.TRANSFEM_SCIENCE) "tfs" else "ese"}"
|
||||
|
||||
/**
|
||||
* Case-insensitive lookup. The ODS asset keys use the biological casing
|
||||
@ -56,11 +58,6 @@ object PKProfileStore {
|
||||
fun profileLength(ester: String, model: String): Int =
|
||||
lookup(profileKey(ester, model))?.size ?: 0
|
||||
|
||||
/**
|
||||
* Normalized response (pg/mL per mg injected) at dtHours after a 1 mg injection,
|
||||
* linearly interpolated between hourly points; beyond the table, extrapolated
|
||||
* with the terminal exponential slope.
|
||||
*/
|
||||
/**
|
||||
* Demi-vie TERMINALE (JOURS) estimée depuis la table horaire (v1.8.0) :
|
||||
* même logique que l'extrapolation de [sample] (dernier point ≥ 1 % du
|
||||
@ -80,14 +77,14 @@ object PKProfileStore {
|
||||
fun terminalHalfLifeDays(ester: String, model: String): Double? {
|
||||
if (!initialized) return null
|
||||
val suffix = when (model) {
|
||||
com.hormonetrack.data.model.PKModels.TRANSFEM_SCIENCE -> "tfs"
|
||||
com.hormonetrack.data.model.PKModels.ESTRANNAISE -> "ese"
|
||||
PKModels.TRANSFEM_SCIENCE -> "tfs"
|
||||
PKModels.ESTRANNAISE -> "ese"
|
||||
else -> return 0.0
|
||||
}
|
||||
val arr = lookup("${ester}_$suffix") ?: return null
|
||||
val (j, rate) = terminalDecayParameters(arr) ?: return null
|
||||
if (rate >= 0.0) return null
|
||||
return kotlin.math.ln(2.0) / -rate / 24.0 // pente /h → t½ en jours
|
||||
return ln(2.0) / -rate / 24.0 // pente /h → t½ en jours
|
||||
}
|
||||
|
||||
/**
|
||||
@ -117,11 +114,16 @@ object PKProfileStore {
|
||||
return j to rate
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalized response (pg/mL per mg injected) at dtHours after a 1 mg injection,
|
||||
* linearly interpolated between hourly points; beyond the table, extrapolated
|
||||
* with the terminal exponential slope.
|
||||
*/
|
||||
fun sample(ester: String, model: String, dtHours: Double): Double {
|
||||
if (!initialized || dtHours <= 0.0) return 0.0
|
||||
val suffix = when (model) {
|
||||
com.hormonetrack.data.model.PKModels.TRANSFEM_SCIENCE -> "tfs"
|
||||
com.hormonetrack.data.model.PKModels.ESTRANNAISE -> "ese"
|
||||
PKModels.TRANSFEM_SCIENCE -> "tfs"
|
||||
PKModels.ESTRANNAISE -> "ese"
|
||||
else -> return 0.0
|
||||
}
|
||||
val arr = lookup("${ester}_$suffix") ?: return 0.0
|
||||
|
||||
@ -165,17 +165,32 @@ object PharmacokineticEngine {
|
||||
*/
|
||||
scalePerEster: Map<String, Double>? = null
|
||||
): Double {
|
||||
// Pré-calculs PAR TRAITEMENT (v1.8.2 — le chemin chaud du tracé labs
|
||||
// appelle e2At par point de grille : 720 pts × N traitements × D doses
|
||||
// recalculaient cutoffHours et batemanParams à chaque itération) :
|
||||
// - cutoff : déterministe par traitement → calculé UNE fois ;
|
||||
// - Bateman (bisection 50 itérations + ln) : inutile pour les
|
||||
// traitements à PROFIL (concentrationOfDose ne lit le paramètre
|
||||
// que dans la branche Bateman, inatteignable quand
|
||||
// usesProfileModel == true) → calcul paresseux.
|
||||
// - doses pré-groupées par traitement (fini le scan complet de
|
||||
// doseLogs par traitement) — l'ordre d'itération des doses d'UN
|
||||
// traitement est préservé (groupBy préserve l'ordre relatif) →
|
||||
// sommation flottante identique.
|
||||
val dosesByTreatment = doseLogs.groupBy { it.treatmentId }
|
||||
val cutoffByTreatment = HashMap<Long, Double>()
|
||||
val batemanCache = HashMap<Long, BatemanParams>()
|
||||
var total = 0.0
|
||||
for (treatment in treatments) {
|
||||
if (treatment.type != TreatmentType.ESTRADIOL) continue
|
||||
if (!batemanCache.containsKey(treatment.id)) {
|
||||
cutoffByTreatment[treatment.id] = cutoffHours(treatment)
|
||||
if (!treatment.usesProfileModel) {
|
||||
batemanCache[treatment.id] = batemanParams(treatment)
|
||||
}
|
||||
for (dose in doseLogs) {
|
||||
if (dose.treatmentId != treatment.id || dose.timestamp > tMs) continue
|
||||
for (dose in dosesByTreatment[treatment.id].orEmpty()) {
|
||||
if (dose.timestamp > tMs) continue
|
||||
val dtH = (tMs - dose.timestamp) / 3_600_000.0
|
||||
if (dtH > cutoffHours(treatment)) continue
|
||||
if (dtH > cutoffByTreatment.getValue(treatment.id)) continue
|
||||
val c = concentrationOfDose(
|
||||
treatment, dose, tMs, batemanCache[treatment.id], modelOverride
|
||||
)
|
||||
@ -292,7 +307,15 @@ object PharmacokineticEngine {
|
||||
val searchStart = maxOf(startMs, earliestDose)
|
||||
|
||||
val batemanCache = HashMap<Long, BatemanParams>()
|
||||
relevantTreatments.forEach { batemanCache[it.id] = batemanParams(it) }
|
||||
relevantTreatments.forEach {
|
||||
// Bateman paresseux (v1.8.2) : inutile pour les traitements à profil
|
||||
if (!it.usesProfileModel) batemanCache[it.id] = batemanParams(it)
|
||||
}
|
||||
// v1.8.2 : cutoffs pré-calculés (déterministes par traitement) et
|
||||
// doses pré-groupées — finis les scans complets de doseLogs à chaque
|
||||
// point × traitement (l'ordre de sommation est préservé, cf e2At)
|
||||
val cutoffByTreatment = relevantTreatments.associate { it.id to cutoffHours(it) }
|
||||
val dosesByTreatment = doseLogs.groupBy { it.treatmentId }
|
||||
|
||||
// Doses triées pour suivre l'« ester actif » le long de la grille (pour le k de T)
|
||||
val sortedEstrogenDoses = doseLogs
|
||||
@ -314,10 +337,10 @@ object PharmacokineticEngine {
|
||||
}
|
||||
val k = activeEster?.let { tKPerEster?.get(it) } ?: tConfig.k
|
||||
for (treatment in relevantTreatments) {
|
||||
val cutoff = cutoffHours(treatment)
|
||||
val cutoff = cutoffByTreatment.getValue(treatment.id)
|
||||
val p = batemanCache[treatment.id]
|
||||
for (dose in doseLogs) {
|
||||
if (dose.treatmentId != treatment.id || dose.timestamp > t) continue
|
||||
for (dose in dosesByTreatment[treatment.id].orEmpty()) {
|
||||
if (dose.timestamp > t) continue
|
||||
val dtH = (t - dose.timestamp) / 3_600_000.0
|
||||
if (dtH > cutoff) continue
|
||||
val c = concentrationOfDose(treatment, dose, t, p, modelOverride)
|
||||
@ -449,10 +472,7 @@ object PharmacokineticEngine {
|
||||
if (predicted > maxPredictionSoFar) maxPredictionSoFar = predicted
|
||||
}
|
||||
return ratiosByEster.mapValues { (_, ratios) ->
|
||||
val sorted = ratios.sorted()
|
||||
val median = if (sorted.size % 2 == 1) sorted[sorted.size / 2]
|
||||
else (sorted[sorted.size / 2 - 1] + sorted[sorted.size / 2]) / 2.0
|
||||
(median * 100).toInt() / 100.0
|
||||
medianRounded(ratios, 2)
|
||||
}
|
||||
}
|
||||
|
||||
@ -508,12 +528,7 @@ object PharmacokineticEngine {
|
||||
}
|
||||
if (e2 > maxE2SoFar) maxE2SoFar = e2
|
||||
}
|
||||
return ksByEster.mapValues { (_, ks) ->
|
||||
val sorted = ks.sorted()
|
||||
val median = if (sorted.size % 2 == 1) sorted[sorted.size / 2]
|
||||
else (sorted[sorted.size / 2 - 1] + sorted[sorted.size / 2]) / 2.0
|
||||
(median * 1000).toInt() / 1000.0
|
||||
}
|
||||
return ksByEster.mapValues { (_, ks) -> medianRounded(ks, 3) }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -602,6 +617,20 @@ object PharmacokineticEngine {
|
||||
* jusqu'ici (les labs doivent être traités par timestamp CROISSANT) ;
|
||||
* 0 = premier lab → seule la garde > 0,5 s'applique.
|
||||
*/
|
||||
|
||||
/**
|
||||
* MÉDIANE arrondie d'une liste de ratios (helper partagé par les 4
|
||||
* pipelines de calibration — était dupliqué 4×). Liste triée en place.
|
||||
* @param decimals nombre de décimales après arrondi (via troncature ×10^n)
|
||||
*/
|
||||
private fun medianRounded(values: MutableList<Double>, decimals: Int): Double {
|
||||
values.sort()
|
||||
val median = if (values.size % 2 == 1) values[values.size / 2]
|
||||
else (values[values.size / 2 - 1] + values[values.size / 2]) / 2.0
|
||||
val factor = Math.pow(10.0, decimals.toDouble())
|
||||
return (median * factor).toInt() / factor
|
||||
}
|
||||
|
||||
fun labIsSignificant(predicted: Double, maxSoFar: Double): Boolean =
|
||||
predicted > 0.5 &&
|
||||
(maxSoFar <= 0.0 || predicted >= LAB_MIN_PREDICTION_FRACTION * maxSoFar)
|
||||
@ -636,13 +665,7 @@ object PharmacokineticEngine {
|
||||
if (predicted > maxPredictionSoFar) maxPredictionSoFar = predicted
|
||||
}
|
||||
if (ratios.isEmpty()) return null
|
||||
ratios.sort()
|
||||
val median = if (ratios.size % 2 == 1) {
|
||||
ratios[ratios.size / 2]
|
||||
} else {
|
||||
(ratios[ratios.size / 2 - 1] + ratios[ratios.size / 2]) / 2.0
|
||||
}
|
||||
return (median * 100).toInt() / 100.0
|
||||
return medianRounded(ratios, 2)
|
||||
}
|
||||
|
||||
fun computeTConfigCalibration(
|
||||
@ -651,8 +674,10 @@ object PharmacokineticEngine {
|
||||
doseLogs: List<DoseLog>,
|
||||
current: TConfig
|
||||
): TConfig? {
|
||||
// Le premier filtre « brut > floor » est soussumé par celui sur la
|
||||
// valeur CONVERTIE (toute conversion est ≤ la valeur brute) : un seul
|
||||
// filtre après conversion suffit — résultat strictement identique.
|
||||
val usable = tLabs
|
||||
.filter { it.value > current.floor + 0.02 }
|
||||
.map { it.copy(value = convertTToNgMl(it.value, it.unit)) }
|
||||
.filter { it.value > current.floor + 0.02 }
|
||||
if (usable.isEmpty()) return null
|
||||
@ -665,13 +690,7 @@ object PharmacokineticEngine {
|
||||
if (k > 1e-4 && k < 10.0) ks.add(k)
|
||||
}
|
||||
if (ks.isEmpty()) return null
|
||||
ks.sort()
|
||||
val median = if (ks.size % 2 == 1) {
|
||||
ks[ks.size / 2]
|
||||
} else {
|
||||
(ks[ks.size / 2 - 1] + ks[ks.size / 2]) / 2.0
|
||||
}
|
||||
return current.copy(k = (median * 1000).toInt() / 1000.0)
|
||||
return current.copy(k = medianRounded(ks, 3))
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -20,7 +20,6 @@ import com.hormonetrack.R
|
||||
import com.hormonetrack.data.AppDatabase
|
||||
import com.hormonetrack.pk.Alerts
|
||||
import com.hormonetrack.pk.PharmacokineticEngine
|
||||
import com.hormonetrack.pk.TConfig
|
||||
import com.hormonetrack.util.AppLog
|
||||
import kotlinx.coroutines.flow.first
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@ -5,6 +5,7 @@ import android.content.Context
|
||||
import android.content.ContentValues
|
||||
import android.provider.CalendarContract
|
||||
import com.hormonetrack.data.model.Treatment
|
||||
import com.hormonetrack.ui.components.formatDose
|
||||
import java.util.Calendar
|
||||
import java.util.TimeZone
|
||||
|
||||
@ -188,6 +189,6 @@ object CalendarEvents {
|
||||
private fun formatInterval(treatment: Treatment): String =
|
||||
treatment.forecastIntervalDays?.let { formatDose(it) } ?: "?"
|
||||
|
||||
private fun formatDose(d: Double): String =
|
||||
if (d == d.toLong().toDouble()) d.toLong().toString() else "%.1f".format(d)
|
||||
// formatDose est la fonction PUBLIQUE de ui/components/DoseDialog.kt
|
||||
// (dédupliquée v1.8.2 — la même règle d'affichage des doses partout)
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
object ReminderContract {
|
||||
const val CHANNEL_ID = "hormonetrack_reminders"
|
||||
@ -215,7 +216,7 @@ class ReminderReceiver : BroadcastReceiver() {
|
||||
// n'existait qu'après un reboot/une édition. Le calcul passe par le moteur
|
||||
// (grille Posologie si elle existe, sinon quotidien).
|
||||
val pending = goAsync()
|
||||
kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.SupervisorJob() + kotlinx.coroutines.Dispatchers.IO).launch {
|
||||
CoroutineScope(SupervisorJob() + Dispatchers.IO).launch {
|
||||
try {
|
||||
val db = AppDatabase.getInstance(context)
|
||||
db.treatmentDao().getById(treatmentId)?.let { tr ->
|
||||
@ -238,10 +239,10 @@ class BootReceiver : BroadcastReceiver() {
|
||||
// TOUS les traitements : les inactifs doivent voir leur alarme
|
||||
// annulée au boot (sinon une alarme périmée continue de sonner) ;
|
||||
// les doses alimentent la grille Posologie (fix #52)
|
||||
val treatments = kotlinx.coroutines.runBlocking {
|
||||
val treatments = runBlocking {
|
||||
db.treatmentDao().getAllOnce()
|
||||
}
|
||||
val doses = kotlinx.coroutines.runBlocking {
|
||||
val doses = runBlocking {
|
||||
db.doseLogDao().getAllOnce()
|
||||
}
|
||||
AlarmScheduler(context).rescheduleAll(treatments, doses)
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
package com.hormonetrack.settings
|
||||
|
||||
/** Précompilées (v1.8.2) : appelées à chaque ouverture du dialog « Nouveautés » */
|
||||
private val VERSION_PATTERN = Regex("^\\d+(\\.\\d+)*$")
|
||||
private val SECTION_PATTERN = Regex("## \\[(\\d+\\.\\d+\\.\\d+)\\][^\\n]*\\n(.*?)(?=\\n## \\[|\\Z)", RegexOption.DOT_MATCHES_ALL)
|
||||
|
||||
/**
|
||||
* Utilitaires pour le dialog « Nouveautés » affiché après chaque mise à jour
|
||||
* (v1.3.0). Pur (aucune dépendance Android) → testable en JVM.
|
||||
@ -28,7 +32,7 @@ object ChangelogHelper {
|
||||
|
||||
private fun parse(version: String): List<Int>? {
|
||||
val cleaned = version.trim().removePrefix("v")
|
||||
if (!Regex("^\\d+(\\.\\d+)*$").matches(cleaned)) return null
|
||||
if (!VERSION_PATTERN.matches(cleaned)) return null
|
||||
return cleaned.split(".").map { it.toInt() }
|
||||
}
|
||||
|
||||
@ -43,7 +47,7 @@ object ChangelogHelper {
|
||||
* @return le markdown concaténé des nouveautés ("" si rien de nouveau).
|
||||
*/
|
||||
fun sectionsSince(markdown: String, lastSeenVersion: String?, currentVersion: String): String {
|
||||
val pattern = Regex("## \\[(\\d+\\.\\d+\\.\\d+)\\][^\\n]*\\n(.*?)(?=\\n## \\[|\\Z)", RegexOption.DOT_MATCHES_ALL)
|
||||
val pattern = SECTION_PATTERN
|
||||
val out = StringBuilder()
|
||||
for (match in pattern.findAll(markdown)) {
|
||||
val version = match.groupValues[1]
|
||||
|
||||
@ -17,8 +17,6 @@ import com.hormonetrack.data.model.DoseLog
|
||||
import com.hormonetrack.data.model.LabResult
|
||||
import com.hormonetrack.pk.LevelPoint
|
||||
import com.hormonetrack.pk.PharmacokineticEngine
|
||||
import com.hormonetrack.ui.theme.ChartE2
|
||||
import com.hormonetrack.ui.theme.ChartT
|
||||
import com.hormonetrack.ui.theme.LabDot
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
@ -373,6 +371,12 @@ fun CurveChart(
|
||||
(markerColor.blue * 255).toInt()
|
||||
)
|
||||
}
|
||||
// SimpleDateFormat créé UNE fois (hors boucle) : le Canvas se
|
||||
// redessine à chaque pan/zoom, une allocation par dose × frame
|
||||
// était du gaspillage (la classe n'est de toute façon pas
|
||||
// thread-safe — ici on reste dans le DrawScope UI)
|
||||
val hourFmt = SimpleDateFormat("HH:mm", Locale.getDefault())
|
||||
hourFmt.timeZone = zone
|
||||
for (dose in forecastDoses) {
|
||||
if (dose.timestamp !in t0..t1) continue
|
||||
val x = xOf(dose.timestamp)
|
||||
@ -392,8 +396,6 @@ fun CurveChart(
|
||||
}
|
||||
drawPath(path, markerColor)
|
||||
// heure de la dose simulée DANS le fuseau de lecture
|
||||
val hourFmt = SimpleDateFormat("HH:mm", Locale.getDefault())
|
||||
hourFmt.timeZone = zone
|
||||
drawContext.canvas.nativeCanvas.drawText(
|
||||
hourFmt.format(Date(dose.timestamp)), x, padTop + 3.dp.toPx(), paint
|
||||
)
|
||||
@ -516,7 +518,7 @@ fun CurveChart(
|
||||
* estimée de l'extremum (v1.2.6) : valeur au-dessus d'un pic, en dessous d'un
|
||||
* creux — dans la couleur de la série, unité implicite (celle de l'axe).
|
||||
*/
|
||||
private fun DrawScope.drawExtremum(cx: Float, cy: Float, extremum: com.hormonetrack.pk.PharmacokineticEngine.Extremum, color: Color) {
|
||||
private fun DrawScope.drawExtremum(cx: Float, cy: Float, extremum: PharmacokineticEngine.Extremum, color: Color) {
|
||||
val r = 5.dp.toPx()
|
||||
val offset = 6.dp.toPx()
|
||||
val path = Path()
|
||||
@ -574,7 +576,8 @@ private fun DrawScope.drawAxisUnit(
|
||||
drawContext.canvas.nativeCanvas.drawText(text, x, baseline, labelPaint(align))
|
||||
}
|
||||
|
||||
private fun DrawScope.drawGrid(color: Color, padLeft: Float, padTop: Float, w: Float, h: Float, rows: Int) { for (i in 0..rows) {
|
||||
private fun DrawScope.drawGrid(color: Color, padLeft: Float, padTop: Float, w: Float, h: Float, rows: Int) {
|
||||
for (i in 0..rows) {
|
||||
val y = padTop + h * i / rows
|
||||
drawLine(color, Offset(padLeft, y), Offset(padLeft + w, y), strokeWidth = 1f)
|
||||
}
|
||||
|
||||
@ -42,7 +42,6 @@ import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.hormonetrack.R
|
||||
import com.hormonetrack.data.model.Treatment
|
||||
import com.hormonetrack.pk.LevelPoint
|
||||
import com.hormonetrack.pk.LabTrajectoryModel
|
||||
import com.hormonetrack.pk.PharmacokineticEngine
|
||||
@ -62,6 +61,12 @@ import com.hormonetrack.ui.theme.TealTertiary
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import com.hormonetrack.ui.components.clampPanHours
|
||||
import com.hormonetrack.ui.components.defaultModelToggles
|
||||
import com.hormonetrack.ui.components.forecastExtensionHours
|
||||
import com.hormonetrack.ui.components.forecastHorizonHours
|
||||
import com.hormonetrack.ui.components.panDeltaHours
|
||||
import com.hormonetrack.ui.components.stepForRange
|
||||
|
||||
private const val HOUR_MS = 3_600_000L
|
||||
// Zoom (v1.2.9) : plage temporelle bornée 6 h → 300 j
|
||||
@ -72,10 +77,12 @@ private const val MAX_RANGE_H = 24L * 300
|
||||
* Écran Graphiques.
|
||||
*
|
||||
* Fonctions :
|
||||
* - plages 24 h / 7 j / 30 j ;
|
||||
* - **panoramique** : glisser horizontalement déplace la fenêtre dans le passé ;
|
||||
* - **les deux modèles PK** (Estrannaise / Transfem Science) peuvent être affichés
|
||||
* simultanément (override de modèle dans le moteur), avec toggle indépendant ;
|
||||
* - plages 6 h → 300 j (zoom v1.2.9, presets 24 h / 7 j / 30 j) ;
|
||||
* - **panoramique** : glisser horizontalement déplace la fenêtre dans le passé
|
||||
* (le futur avec la Prévision active) ;
|
||||
* - **les modèles PK** (Estrannaise / Transfem Science / WHSAH) peuvent être
|
||||
* affichés simultanément (override de modèle dans le moteur), toggles
|
||||
* indépendants ; + Tracé labs (v1.5.0) et sa prolongation (v1.6.0) ;
|
||||
* - **prévision** : prolonge la courbe avec des doses projetées à partir de la
|
||||
* « fréquence » configurée sur chaque traitement ;
|
||||
* - **auto-calibration** (option des Paramètres) : facteurs d'échelle et modèle T
|
||||
@ -135,7 +142,7 @@ fun ChartScreen() {
|
||||
var modelTogglesInitialized by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(treatments) {
|
||||
if (!modelTogglesInitialized && treatments.isNotEmpty()) {
|
||||
val toggles = com.hormonetrack.ui.components.defaultModelToggles(
|
||||
val toggles = defaultModelToggles(
|
||||
treatments.filter { it.usesProfileModel }.map { it.pkModel }.toSet()
|
||||
)
|
||||
showEse = toggles.getValue("ESE")
|
||||
@ -194,7 +201,7 @@ fun ChartScreen() {
|
||||
// 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 forecastHorizonHours = com.hormonetrack.ui.components.forecastHorizonHours(maxIntervalDays)
|
||||
val forecastHorizonHours = forecastHorizonHours(maxIntervalDays)
|
||||
|
||||
// ── Fenêtre temporelle (v1.4.1, extension v1.4.4, stabilisation v1.4.5) ──
|
||||
// ⚠️ BOUCLE DE RECOMPOSITION (fix #56) : `nowMs` doit être MÉMOÏSÉ sur le
|
||||
@ -220,7 +227,7 @@ fun ChartScreen() {
|
||||
}
|
||||
}
|
||||
val forecastExtensionH = if (showForecast && forecastHorizonHours != null) {
|
||||
com.hormonetrack.ui.components.forecastExtensionHours(
|
||||
forecastExtensionHours(
|
||||
rangeHours, forecastHorizonHours, firstSlotGapH
|
||||
)
|
||||
} else 0L
|
||||
@ -269,7 +276,7 @@ fun ChartScreen() {
|
||||
val curves by produceState(
|
||||
emptyList<Pair<String, List<LevelPoint>>>(),
|
||||
autoByModel, doseLogs, startMs, endMs, showEse, showTfs, showWhs, showForecast,
|
||||
showLabTrack, showLabTrackExtend, labResults.size
|
||||
showLabTrack, showLabTrackExtend, labResults
|
||||
) {
|
||||
withContext(Dispatchers.Default) {
|
||||
val list = mutableListOf<Pair<String, List<LevelPoint>>>()
|
||||
@ -290,7 +297,7 @@ fun ChartScreen() {
|
||||
// non vides seulement — la légende (plus bas) doit connaître
|
||||
// "LABX" AVANT son else (leçon #63).
|
||||
if (showLabTrack) {
|
||||
val step = com.hormonetrack.ui.components.stepForRange(rangeHours)
|
||||
val step = stepForRange(rangeHours)
|
||||
val labCurve = LabTrajectoryModel.computeLabAnchoredCurve(
|
||||
treatments, doseLogs, labResults, startMs, endMs, step,
|
||||
extendBeyondLastLab = showLabTrackExtend
|
||||
@ -319,39 +326,18 @@ fun ChartScreen() {
|
||||
} else emptyList()
|
||||
val allDoses = doseLogs + forecastDoses
|
||||
// Pas d'échantillonnage adaptatif au zoom (v1.2.9)
|
||||
val step = com.hormonetrack.ui.components.stepForRange(rangeHours)
|
||||
if (showEse) {
|
||||
val autoM = autoByModel["ESE"]
|
||||
val step = stepForRange(rangeHours)
|
||||
// v1.8.2 : les 3 blocs copiés (ESE/TFS/WHS) sont factorisés —
|
||||
// le modèle est la seule variable (même calcul, même calibration
|
||||
// PAR MODÈLE via autoByModel[model], fix #60)
|
||||
for ((model, enabled) in listOf("ESE" to showEse, "TFS" to showTfs, "WHS" to showWhs)) {
|
||||
if (!enabled) continue
|
||||
val autoM = autoByModel[model]
|
||||
list.add(
|
||||
"ESE" to PharmacokineticEngine.computeCurve(
|
||||
model to PharmacokineticEngine.computeCurve(
|
||||
effTreatments, allDoses, startMs, endMs,
|
||||
stepMs = step,
|
||||
tConfig = autoM?.tConfig ?: effTConfig, modelOverride = "ESE",
|
||||
scalePerEster = autoM?.esterScales,
|
||||
tKPerEster = autoM?.tKPerEster
|
||||
)
|
||||
)
|
||||
}
|
||||
if (showTfs) {
|
||||
val autoM = autoByModel["TFS"]
|
||||
list.add(
|
||||
"TFS" to PharmacokineticEngine.computeCurve(
|
||||
effTreatments, allDoses, startMs, endMs,
|
||||
stepMs = step,
|
||||
tConfig = autoM?.tConfig ?: effTConfig, modelOverride = "TFS",
|
||||
scalePerEster = autoM?.esterScales,
|
||||
tKPerEster = autoM?.tKPerEster
|
||||
)
|
||||
)
|
||||
}
|
||||
// v1.4.6 : 3ᵉ modèle WHSAH (même mécanique d'override)
|
||||
if (showWhs) {
|
||||
val autoM = autoByModel["WHS"]
|
||||
list.add(
|
||||
"WHS" to PharmacokineticEngine.computeCurve(
|
||||
effTreatments, allDoses, startMs, endMs,
|
||||
stepMs = step,
|
||||
tConfig = autoM?.tConfig ?: effTConfig, modelOverride = "WHS",
|
||||
tConfig = autoM?.tConfig ?: effTConfig, modelOverride = model,
|
||||
scalePerEster = autoM?.esterScales,
|
||||
tKPerEster = autoM?.tKPerEster
|
||||
)
|
||||
@ -550,12 +536,12 @@ fun ChartScreen() {
|
||||
// v1.4.10 (fix #62) : cumul fractionnaire du pan
|
||||
// (l'ancienne troncature par delta tuait le pan
|
||||
// sur la vue 24 h)
|
||||
val (hoursDragged, newResidual) = com.hormonetrack.ui.components.panDeltaHours(
|
||||
val (hoursDragged, newResidual) = panDeltaHours(
|
||||
pan.x, chartWidthPx, rangeHours, panResidualHours
|
||||
)
|
||||
panResidualHours = newResidual
|
||||
if (hoursDragged != 0L) {
|
||||
panHours = com.hormonetrack.ui.components.clampPanHours(
|
||||
panHours = clampPanHours(
|
||||
panHours + hoursDragged, gestureFutureHorizon, gestureMaxPan
|
||||
)
|
||||
}
|
||||
@ -573,7 +559,7 @@ fun ChartScreen() {
|
||||
val tFocal = endAnchor - ((1.0 - f) * rangeHours * HOUR_MS).toLong()
|
||||
val newEnd = tFocal + ((1.0 - f) * newRange * HOUR_MS).toLong()
|
||||
val newPan = ((nowMs - newEnd) / HOUR_MS).toLong()
|
||||
panHours = com.hormonetrack.ui.components.clampPanHours(
|
||||
panHours = clampPanHours(
|
||||
newPan, gestureFutureHorizon, gestureMaxPan
|
||||
)
|
||||
rangeHours = newRange
|
||||
|
||||
@ -5,10 +5,8 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
@ -35,13 +33,11 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.hormonetrack.R
|
||||
import com.hormonetrack.data.model.DoseLog
|
||||
import com.hormonetrack.data.model.Treatment
|
||||
import com.hormonetrack.ui.LocalAppContainer
|
||||
import com.hormonetrack.ui.components.DoseDialog
|
||||
import com.hormonetrack.util.HrtDuration
|
||||
|
||||
@ -72,7 +72,7 @@ import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
import kotlin.math.abs
|
||||
|
||||
private val HOUR_MS = 3_600_000L
|
||||
private const val HOUR_MS = 3_600_000L
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@ -128,7 +128,10 @@ fun HomeScreen(
|
||||
}
|
||||
}
|
||||
|
||||
val curve by produceState<List<LevelPoint>>(emptyList(), effectiveAuto, doseLogs, tick) {
|
||||
// v1.8.2 : allTreatments et tConfig ajoutés aux clés — éditer un
|
||||
// scaleFactor ou le modèle T rafraîchit la courbe immédiatement
|
||||
// (l'ancien trio de clés ratait ces changements jusqu'au tick de 60 s)
|
||||
val curve by produceState<List<LevelPoint>>(emptyList(), allTreatments, tConfig, effectiveAuto, doseLogs, tick) {
|
||||
withContext(Dispatchers.Default) {
|
||||
val end = System.currentTimeMillis()
|
||||
value = PharmacokineticEngine.computeCurve(
|
||||
@ -463,8 +466,7 @@ private fun NextDoseCard(treatments: List<Treatment>, doseLogs: List<DoseLog>) {
|
||||
val fires = treatments
|
||||
.mapNotNull { tr -> PharmacokineticEngine.nextReminderFireFor(tr, doseLogs, nowMs)?.let { tr to it } }
|
||||
if (fires.isEmpty()) return
|
||||
val next = fires.minByOrNull { it.second }!!.second
|
||||
val nextTreatment = fires.minByOrNull { it.second }!!.first
|
||||
val (nextTreatment, next) = fires.minByOrNull { it.second }!!
|
||||
Card(Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package com.hormonetrack.ui.screens
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
|
||||
@ -144,13 +144,16 @@ fun SettingsScreen(onBack: () -> Unit) {
|
||||
// v1.4.2 : le backup embarque aussi les PARAMÈTRES utilisateur
|
||||
// (langue, auto-calibration, seuils d'alerte) — collect des
|
||||
// DataStore Flows (first()) avant sérialisation.
|
||||
// UNE seule collecte des seuils (les 4 champs viennent du même
|
||||
// snapshot DataStore — 4 × .first() = 4 collectes redondantes)
|
||||
val thresholds = container.settings.alertThresholds.first()
|
||||
val settings = com.hormonetrack.data.backup.UserSettings(
|
||||
language = container.settings.language.first(),
|
||||
autoCalibrate = container.settings.autoCalibrate.first(),
|
||||
alertE2High = container.settings.alertThresholds.first().e2High,
|
||||
alertE2Low = container.settings.alertThresholds.first().e2Low,
|
||||
alertTHigh = container.settings.alertThresholds.first().tHigh,
|
||||
alertTLow = container.settings.alertThresholds.first().tLow
|
||||
alertE2High = thresholds.e2High,
|
||||
alertE2Low = thresholds.e2Low,
|
||||
alertTHigh = thresholds.tHigh,
|
||||
alertTLow = thresholds.tLow
|
||||
)
|
||||
val json = BackupManager.exportJson(repo, tConfig, settings)
|
||||
val ok = BackupManager.writeBackup(context, uri, json)
|
||||
|
||||
@ -52,7 +52,6 @@ import com.hormonetrack.data.model.PKPresets
|
||||
import com.hormonetrack.data.model.Treatment
|
||||
import com.hormonetrack.data.model.TreatmentType
|
||||
import com.hormonetrack.pk.PharmacokineticEngine
|
||||
import android.app.Activity
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import com.hormonetrack.reminder.AlarmScheduler
|
||||
|
||||
@ -60,9 +60,9 @@ object AppLog {
|
||||
fun log(tag: String, message: String) = synchronized(lock) {
|
||||
val line = formatLine(System.currentTimeMillis(), tag, message)
|
||||
buffer.addLast(line)
|
||||
val trimmed = trimLines(buffer.toList(), MAX_LINES)
|
||||
buffer.clear()
|
||||
buffer.addAll(trimmed)
|
||||
// Purge O(1) amortie (v1.8.2 : l'ancien trimLines recopiait TOUT le
|
||||
// buffer à chaque ligne loggée) — même résultat (MAX_LINES dernières)
|
||||
while (buffer.size > MAX_LINES) buffer.removeFirst()
|
||||
try {
|
||||
file?.appendText(line + "\n")
|
||||
} catch (_: Exception) {
|
||||
|
||||
@ -3,6 +3,43 @@
|
||||
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.8.2] — 2026-09-17 (versionCode 37)
|
||||
|
||||
### Maintenance — performance du moteur + fraîcheur de l'interface
|
||||
|
||||
Aucune nouvelle fonctionnalité : session d'audit complet (code + doc), avec
|
||||
les optimisations et corrections sûres qui en sont sorties.
|
||||
|
||||
### Performance du moteur PK
|
||||
- `e2At`/`computeCurve` : `cutoffHours` et les paramètres Bateman
|
||||
(bisection 50 itérations) ne sont plus recalculés à chaque (point ×
|
||||
dose × traitement) — pré-calculés par traitement, et Bateman devient
|
||||
**paresseux** (inutile pour les traitements à profil). Le chemin le plus
|
||||
chaud (tracé labs : ~720 points × N × D appels de cutoff par refresh)
|
||||
s'allège d'un facteur P × T. Doses pré-groupées par traitement (fin des
|
||||
scans complets). **Résultat identique** (ordre de sommation préservé —
|
||||
223 tests verts inchangés).
|
||||
- `AppLog` : purge du buffer sans recopie à chaque ligne loggée.
|
||||
- `ChangelogHelper` : Regex précompilées (dialog « Nouveautés »).
|
||||
- `CurveChart` : SimpleDateFormat créé hors boucle de dessin.
|
||||
|
||||
### Corrigé — fraîcheur de l'interface
|
||||
- **Accueil** : éditer un scaleFactor ou le modèle T rafraîchit la courbe
|
||||
immédiatement (clés du producteur incomplètes — l'ancien trio attendait
|
||||
le tick de 60 s).
|
||||
- **Graphiques** : éditer la VALEUR d'une analyse rafraîchit les courbes
|
||||
(la clé n'était que le NOMBRE d'analyses).
|
||||
|
||||
### Maintenance — code & documentation
|
||||
- Code mort supprimé (Repository/DAO sans appelants, `currentLevel`,
|
||||
variable et scheduler dupliqués), imports morts/redondants nettoyés,
|
||||
médiane de calibration factorisée (4 copies → 1 helper),
|
||||
qualifications raccourcies, en-têtes/KDoc obsolètes mis à jour.
|
||||
- Documentation : ~35 corrections (compteurs de tests obsolètes, TOC et
|
||||
ancres réparées, inventaire des suites de tests à jour et fusionné,
|
||||
titres CHANGELOG doublés supprimés, versions « à jour v1.2.0 » /
|
||||
« Gradle 8.9 » / « API 34 » corrigées, fautes).
|
||||
|
||||
## [1.8.1] — 2026-09-17 (versionCode 36)
|
||||
|
||||
### Corrigé — la stabilisation ignorait les changements récents (dose/ester/intervalle)
|
||||
@ -224,8 +261,6 @@ Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.4.1
|
||||
|
||||
## [1.4.9] — 2026-09-07 (versionCode 29)
|
||||
|
||||
## [1.4.9] — 2026-09-07 (versionCode 29)
|
||||
|
||||
### Corrigé
|
||||
- **La calibration tombait à ×2,21 (auto ET manuelle) sur un traitement
|
||||
basculé sous un autre modèle** (#61, remontée : « le bouton Calibrer avec
|
||||
@ -233,7 +268,7 @@ Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.4.1
|
||||
diagnostic par reproduction sur données réelles — l'export contient une
|
||||
dose EEn de test logguée en **janvier** ; les labs de janvier-mars
|
||||
tombaient 15–76 j après cette dose, quand la prédiction WHSAH est
|
||||
RÉSIDUELLE (184 → 1 pg/mL) → ratios aberrants (3,3 / 15,9 / 89,6 / 383)
|
||||
RÉSIDUELLE (184 → 1 pg/mL) → ratios aberrants (3,3 / 15,9 / 89,6 / 383 / 391)
|
||||
→ la MÉDIANE tombait à ×2,21, **dans les deux pipelines** (manuel ET
|
||||
auto — même défaut de frontière). FIX : **garde de significativité**
|
||||
(`labIsSignificant`) — un lab ne calibre que si la prédiction reste
|
||||
@ -253,8 +288,6 @@ Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.4.1
|
||||
|
||||
## [1.4.8] — 2026-09-07 (versionCode 28)
|
||||
|
||||
## [1.4.8] — 2026-09-07 (versionCode 28)
|
||||
|
||||
### Corrigé
|
||||
- **La calibration automatique était fausse pour les modèles autres que le
|
||||
modèle stocké du traitement** (#60, remontée : « la calibration auto sur
|
||||
|
||||
@ -11,23 +11,23 @@
|
||||
|
||||
1. [Contexte & objectifs](#1-contexte--objectifs)
|
||||
2. [Historique du projet](#2-historique-du-projet)
|
||||
3. [Stack & versions (épinglées)](#3-stack--versions-épinglées)
|
||||
3. [Stack & versions (épinglées — à jour v1.8.1)](#3-stack--versions-épinglées--à-jour-v181)
|
||||
4. [Environnement de build (cette machine)](#4-environnement-de-build-cette-machine)
|
||||
5. [Architecture générale](#5-architecture-générale)
|
||||
6. [Modèle de données (Room)](#6-modèle-de-données-room) + [6.bis Sémantique isActive](#6bis-sémantique-isactive-v124--drapeau-administratif-jamais-un-filtre)
|
||||
7. [Moteur pharmacocinétique](#7-moteur-pharmacocinétique) + [7.8 Modèle TFS V3C](#78-modèle-tfs-v3c-v140--pktransfemsciencemodelskt) + [7.9 Modèle WHSAH](#79-modèle-whsah-v146--pkwhsahmodelskt) + [7.10 « Tracé labs »](#710--tracé-labs--v150--pklabtrajectorymodelkt) + [7.10.bis Prolongation](#710bis-prolongation-au-delà-du-dernier-lab-v160--pklabtrajectorymodelkt)
|
||||
8. [Tests unitaires](#8-tests-unitaires) + [8.bis Données de test hors dépôt](#8bis-données-de-test-réelles--hors-dépôt-local-test-data)
|
||||
9. [Système de rappels](#9-système-de-rappels) + [9.bis Alertes de seuil](#9bis-alertes-de-seuil-reminderalertnotifierkt--worker-v142)
|
||||
7. [Moteur pharmacocinétique](#7-moteur-pharmacocinétique) + [7.8 Modèle TFS V3C](#78-modèle-tfs-v3c-v140--pktransfemsciencemodelskt) + [7.9 Modèle WHSAH](#79-modèle-whsah-v146--pkwhsahmodelskt) + [7.10 « Tracé labs »](#710--tracé-labs--v150--pklabtrajectorymodelkt) + [7.10.bis Prolongation](#710bis-prolongation-au-delà-du-dernier-lab-v160--pklabtrajectorymodelkt) + [7.11 Prise de sang recommandée](#711-recommandation-de-prochaine-prise-de-sang-v180--pklabtimingkt)
|
||||
8. [Tests unitaires](#8-tests-unitaires) + [8.bis Données de test réelles : HORS dépôt](#8bis-données-de-test-réelles--hors-dépôt-local-test-data)
|
||||
9. [Système de rappels](#9-système-de-rappels) + [9.bis Alertes de seuil](#9bis-alertes-de-seuil--reminderalertnotifierkt--worker-v142)
|
||||
10. [UI & navigation](#10-ui--navigation)
|
||||
11. [Graphiques (CurveChart)](#11-graphiques-curvechart)
|
||||
12. [i18n FR/EN](#12-i18n-fren)
|
||||
13. [Sauvegarde JSON](#13-sauvegarde-json)
|
||||
13. [Sauvegarde JSON](#13-sauvegarde-json) + [13.bis Auto-backup quotidien](#13bis-sauvegarde-automatique-quotidienne-v170--databackupautobackupworkerkt)
|
||||
14. [Bugs corrigés (historique complet — à ne pas réintroduire)](#14-bugs-corrigés)
|
||||
15. [Comment régénérer l'asset pk_profiles.json](#15-comment-régénérer-lasset-pk_profilesjson)
|
||||
16. [Workflow build / test / install](#16-workflow-build--test--install) + [16.bis Releases Gitea](#16bis-releases-gitea-avec-apk-téléchargeable) + [16.ter Test sur émulateur](#16ter-recette-test-manuel-sur-émulateur-reproductible--v134)
|
||||
16. [Workflow build / test / install / git](#16-workflow-build--test--install--git) + [16.bis Releases Gitea](#16bis-releases-gitea-avec-apk-téléchargeable) + [16.ter Recette : test sur émulateur](#16ter-recette--test-manuel-sur-émulateur-reproductible--v134)
|
||||
17. [Montre : Gadgetbridge & options](#17-montre--gadgetbridge--options)
|
||||
18. [Espace disque & coûts](#18-espace-disque--coûts)
|
||||
19. [Limites connues & choix volontaires](#19-limites-connues)
|
||||
19. [Limites connues](#19-limites-connues)
|
||||
20. [Idées d'évolution (Phase 2+)](#20-idées-dévolution)
|
||||
21. [Checklist de test manuel](#21-checklist-de-test-manuel)
|
||||
|
||||
@ -63,6 +63,7 @@ Fonctionnalités v1 :
|
||||
|
||||
| Date | Événement |
|
||||
|---|---|
|
||||
| 17 sept. 2026 (session v1.8.2) | **Maintenance — audit complet code + doc** : (a) **perf moteur** : cutoffHours pré-calculé par traitement dans e2At/computeCurve (le chemin chaud du tracé labs recalculait ~430 k appels/refresh), Bateman paresseux (inutile pour les traitements à profil), doses pré-groupées par traitement — résultat identique (ordre de sommation préservé, 223 tests verts inchangés) ; (b) **2 fixes de fraîcheur** (#65 accueil : clés produceState sans allTreatments/tConfig — éditer un scaleFactor n'actualisait pas la courbe avant le tick ; #66 graphiques : clé `labResults.size` — éditer la valeur d'un lab ne recalculait pas) ; (c) **nettoyage** : ~9 imports morts, qualifications raccourcies, code mort (Repository/DAO, currentLevel, scheduler2), médiane factorisée (4 copies), SimpleDateFormat hors boucle, AppLog sans recopie, Regex précompilées, formatDose dédupliqué ; (d) **doc** : ~35 corrections (compteurs actuels 223/193 partout, TOC + ancres réparées, inventaire §8 à jour et fusionné, doublons CHANGELOG, §19 backup, fautes). Audit préalable par double exploration lecture-seule (46 fichiers + 4 docs). 223 verts + lint + émulateur smoke. Android seul (web non concerné — re-render complet, aucune release web). |
|
||||
| 17 sept. 2026 (session v1.8.1) | **Fix stabilisation + suggestion sur l'Accueil** (critique v1.8.0 : « changé d'ester, de dosage ET de posologie, et l'app me disait stabilisée depuis février ») : le proxy « 1ʳᵉ dose du traitement » était aveugle aux changements récents. NOUVELLE règle `regimeStartMs` : le régime courant = la **séquence terminale de doses** à (ester effectif, dose mg, **écart inter-doses**) constants (l'écart comparé EXACTEMENT — 7 j ± 1 h casse le régime) ; tout changement récent réinitialise la stabilisation → la reco saute au premier creux post-stabilisation ; l'ester EFFECTIF de la dernière dose (override compris) alimente la carte. **+ Suggestion aussi sur l'Accueil** (demande) : carte compacte (creux daté + créneau, mention de stabilisation seulement si le régime n'était pas déjà stable). 3 nouveaux tests (223 verts / 193 sans données locales). Validé émulateur (release, seed réel) : la carte passe de « stabilisé depuis le 06/02 » à « EEN pas stabilisé avant le 29/09 — premier creux fiable », creux 27/09 22:43, 0 crash. Web v1.8.1 sync (3 tests miroirs + encart Accueil). |
|
||||
| 17 sept. 2026 (session v1.8.0) | **Recommandation de prochaine prise de sang (page Analyses, Android + web sync)** (demande + choix validés) : carte « Prochaine prise de sang (suggestion) » — creux exact calculé sur la courbe prévisionnelle **juste avant l'injection suivante** (+ mention « ou la veille du créneau »), **saut au premier creux STABILISÉ** (5 × t½ terminale — pas de prise intermédiaire trompeuse), filtres honnêtes (injectable actif + Posologie requis, creux jamais déjà mesuré, horizon borné → carte cachée), **invite « renseigne une Posologie »** quand un injectable actif en est dépourvu (demande explicite). Moteur : `pk/LabTiming.kt` PUR (miroir web `js/pk/lab-timing.js`) ; `PKProfileStore.terminalHalfLifeDays` (refactor de la pente d'extrapolation de `sample()` — le modèle Estrannaise lit sa t½ dans la table, TFS/WHSAH analytiquement). UI : calcul `produceState` + tick minute, disclaimer, mutuellement exclusive avec l'invite. 11 nouveaux tests (220 verts / 190 sans données locales) + lint + check web (152). Validé émulateur §16.ter (release, seed réel v1.7.0) : « creux 20/09 10:37, injection EEN 20/09 11:20, EEN stabilisé depuis le 06/02 », 0 crash. |
|
||||
| 16 sept. 2026 (session v1.7.1) | **Fix « une seule note sur deux » dans les Analyses (Android + web)** : une prise E2+T avec des notes DISTINCTES (note clinique E2 + note « DHT » sur T — cas réel) n'en affichait qu'UNE (l'autre conservée mais perdue à l'affichage). Cause : affichage « première note non vide du groupe » — hypothèse historique « toutes identiques » (dialog commun) cassée par l'édition unitaire. FIX : `labNotesForDisplay` PUR des deux côtés (notes distinctes → préfixées du marqueur ; identiques → dédupliquées ; vides ignorées) + UI branchée. + **Nouvel export réel v1.7.0** (3 traitements dont CPA oral, 64 doses, 28 labs, notes doses ET labs) → `local-test-data/backup-v1.7.0.json` (gitigné) + **régression n°5** data-driven (7 tests : parsing, double-note épingle le fix, plausibilité moteur avec CPA, prévision 7 j). 11 nouveaux tests (211 verts / 181 sans données locales) + lint + check web. Validé émulateur §16.ter : seed v1.7.0 → écran Analyses affiche « E2 : … » ET « T : … » (dump), 0 crash. Web v1.7.1 sync (+ leçon : oubli d'entrée CHANGELOG = dialog « Nouveautés » vide, attrapé par l'E2E/§7). |
|
||||
@ -94,7 +95,7 @@ Fonctionnalités v1 :
|
||||
| 7 sept. 2026 (session v1.4.3) | **Question pharmacocinétique → 1 bug + 1 confirmation** : « l'estimation ne remonte pas le lendemain de mon injection EEn » → (a) **comportement NORMAL** (contribution J+1 < 12 % du total, plateau d'équilibre — épinglé par la régression n°4 sur le 4ᵉ export réel) ; (b) **bug #53** : le delta « vs il y a 6 h » de l'accueil comparait en réalité il y a 24 h (`firstOrNull` sur la fenêtre 24 h) → helper `pointHoursBefore` + 4 tests. 137 verts. Publication v1.4.3. |
|
||||
| 7 sept. 2026 (session v1.4.4) | **Prévision inaccessible depuis v1.4.1** (#54, remontée) : DEUX causes emboîtées — (a) captures FIGÉES dans `pointerInput(Unit)` (`futurePanHorizon` lu à 0 pour toujours → pan futur interdit) → `rememberUpdatedState` + **garde de source** (`ChartScreenSourceGuardTest`, le lint Compose ne voit pas ce pattern) ; (b) toggle sans effet visible (`endMs = now` → 0 dose générée) → extension contrôlée à droite (`forecastExtensionHours`, jusqu'au 1ᵉʳ créneau, sans déplacer le début). Validé émulateur : drag gauche → panHours < 0, courbe dessinée au-delà de « maintenant » (screencap). 140 verts. Publication v1.4.4. |
|
||||
| 7 sept. 2026 (session v1.4.5) | **« La prévision simule le 13 au lieu du 12 »** (#55) : diagnostic sur données réelles — GÉNÉRATION EXACTE (créneau = dernière dose loguée + 6 j ; subtilité : la dose avait été loguée le LENDENDE de l'injection réelle → dérive d'un jour par cycle, l'app suit les logs) ; illusion causée par l'absence de marqueur + labels X à minuit UTC. FIX : marqueurs de doses (prévisionnels pointillés + réels discrets), labels à minuit LOCAL, fuseau de graphique configurable (Paramètres). **+ #56** : boucle de recomposition (`nowMs` relu à chaque frame → endMs dérive → saturation main thread → taps perdus) → mémoïsé sur le tick. 145 verts. Publication v1.4.5. |
|
||||
| 7 sept. 2026 (session v1.4.6) | **3ᵉ modèle PK « WHSAH »** (demande) : le fit « license-free » du WHSAH Collective publié dans **Mona** (l'app concurrente — son git montre qu'elle utilisait NOS paramètres TFS avant mars 2026) intégré au même niveau : dispatch parallèle (aucun remplacement), 6 presets, dropdown 3 choix, 3ᵉ toggle superposable (vert/violet), PEP non couvert. Comparaison chiffrée TFS vs WHSAF épinglée (EV pic 367 vs 295 ; EEn à J+1 ×3). **+ #57** : l'écran Graphiques défile désormais (rangée de toggles coupée sous le pli, trouvé en émulateur). 155 verts. Publication v1.4.6. |
|
||||
| 7 sept. 2026 (session v1.4.6) | **3ᵉ modèle PK « WHSAH »** (demande) : le fit « license-free » du WHSAH Collective publié dans **Mona** (l'app concurrente — son git montre qu'elle utilisait NOS paramètres TFS avant mars 2026) intégré au même niveau : dispatch parallèle (aucun remplacement), 6 presets, dropdown 3 choix, 3ᵉ toggle superposable (vert/violet), PEP non couvert. Comparaison chiffrée TFS vs WHSAH épinglée (EV pic 367 vs 295 ; EEn à J+1 ×3). **+ #57** : l'écran Graphiques défile désormais (rangée de toggles coupée sous le pli, trouvé en émulateur). 155 verts. Publication v1.4.6. |
|
||||
| 7 sept. 2026 (session v1.4.7) | **Toggles alignés sur les traitements** (#58, demande) : au chargement, seuls les modèles utilisés par des traitements à PROFIL PK (`usesProfileModel`) sont ON (l'oral/AA polluait le set — vu en émulateur avec le CPA) ; garde-fou TFS si aucun profil ; une seule init. **Épinglé : la calibration s'applique à TOUTES les courbes** (par ESTER via doseEster, indépendant du modelOverride — test). 158 verts. Publication v1.4.7. | + **#59** : le chip de la carte de traitement affichait « Estrannaise » pour WHSAH (ternaire à 2 branches → `modelLabelRes` centralisé).
|
||||
| 7 sept. 2026 (session v1.4.8) | **Calibration PAR MODÈLE PK** (#60, remontée : « ×2,21 sur WHSAH alors que l'idéal serait < 1 ») : un seul jeu de facteurs (calculé depuis le modèle stocké) était partagé par toutes les courbes → absurde dès qu'un traitement basculait de modèle. FIX : le graphique calcule une AutoCalibrated PAR modèle affiché (ESE/TFS/WHS — modelOverride propagé à computeEsterScaleFactors et computeTKPerEster) → chaque courbe calibrée passe par les labs. Home inchangé (modèle stocké). Test : chaque courbe calibrée ≈ lab (±2 %), échelles TFS ≠ WHS. 159 verts. Publication v1.4.8. |
|
||||
| 7 sept. 2026 (session v1.4.9) | **Fix #61 — calibration tombant à ×2,21 (auto ET manuelle)** : reproduction par test sur données réelles — une dose EEn de test de JANVIER (10 mg) dans l'export, des labs de janvier-mars à 15–76 j → prédiction WHSAH résiduelle (184 → 1) → ratios aberrants (×3,3 → ×391) → médiane ×2,21, dans les DEUX pipelines. FIX : garde de significativité `labIsSignificant` (prédiction ≥ 15 % du max observé, labs par timestamp croissant) sur les 3 pipelines ; facteur WHSAH retombe à 0,55. + Hint UX (auto ON → facteur manuel ignoré à l'affichage) + nettoyage CHANGELOG (4 titres orphelins doublés [1.4.4–1.4.7]). 168 verts. Publication v1.4.9. |
|
||||
@ -109,7 +110,7 @@ Leçon importante de la session build : **les erreurs de compilation et les bugs
|
||||
(bisection inversée, plancher d'affichage des profils) n'ont été détectés qu'en construisant
|
||||
et en testant** — aucun build n'avait été lancé avant la session 3.
|
||||
|
||||
## 3. Stack & versions (épinglées — à jour v1.2.0)
|
||||
## 3. Stack & versions (épinglées — à jour v1.8.1)
|
||||
|
||||
| Composant | Version | Où |
|
||||
|---|---|---|
|
||||
@ -167,11 +168,11 @@ Notes importantes (v1.2.0) :
|
||||
- **SDK Android** : installé via `brew install --cask android-commandlinetools`
|
||||
→ `/opt/homebrew/share/android-commandlinetools` (524 MB)
|
||||
- licences acceptées : `yes | sdkmanager --licenses`
|
||||
- paquets : `platform-tools`, `platforms;android-34`, `build-tools;34.0.0`
|
||||
- paquets (état actuel, cf §18) : `platform-tools`, `platforms;android-34/36/37`, `build-tools;34.0.0` etc.
|
||||
- **`local.properties`** à la racine (non commité) : `sdk.dir=/opt/homebrew/share/android-commandlinetools`
|
||||
- Gradle 8.9 téléchargé par le wrapper ; caches `~/.gradle` ≈ 1,5 GB
|
||||
- Build validé : `./gradlew assembleDebug testDebugUnitTest` → **BUILD SUCCESSFUL**,
|
||||
APK debug 18 MB (`app/build/outputs/apk/debug/app-debug.apk`)
|
||||
- Gradle 9.7.1 téléchargé par le wrapper ; caches `~/.gradle` ≈ 1,5 GB
|
||||
- Build initial validé (5 sept. 2026) : `./gradlew assembleDebug testDebugUnitTest` →
|
||||
**BUILD SUCCESSFUL** ; l'APK debug actuel pèse ≈ 20 Mo (`app/build/outputs/apk/debug/app-debug.apk`)
|
||||
|
||||
## 5. Architecture générale
|
||||
|
||||
@ -371,7 +372,7 @@ timestamp croissant, les 3 pipelines : manuel, échelles par ester, k T).
|
||||
Au-delà de la fenêtre d'action (prédiction résiduelle), le lab reflète
|
||||
AUTRE CHOSE : une dose EEn de test logguée en janvier + des labs de
|
||||
janvier-mars produisaient des ratios ×3,3 → ×391 et la médiane tombait à
|
||||
×2,21 (dans les DEUX pipelines). Result : facteur WHSAH réel = 0,55.
|
||||
×2,21 (dans les DEUX pipelines). Résultat : facteur WHSAH réel = 0,55.
|
||||
- chaque lab est **attribué à la période d'injection dans laquelle il tombe** =
|
||||
dernière dose E2 ≤ lab (une prise de sang reflète d'abord l'injection qui précède) ;
|
||||
- ratio = lab ÷ prédiction **non calibrée** (toutes doses superposées, scaleFactor forcé 1) ;
|
||||
@ -504,7 +505,7 @@ MAIS passant par tous les points ».
|
||||
- **Indépendante de la calibration** : elle EST sa propre calibration
|
||||
continue (ratio brut, `scaleFactor` stocké ignoré ET `scalePerEster`
|
||||
ignoré — l'inverse serait une DOUBLE correction, épinglé par test).
|
||||
effet : les modèles rapprochés au sein du même ancrage CONVERGENT
|
||||
Conséquence : les modèles rapprochés au sein du même ancrage CONVERGENT
|
||||
(le mi-chemin ESE vs WHS colle à ~2 % — `modelOverride` garde une
|
||||
empreinte intermédiaire, testé avec le seuil du test moteur WHS).
|
||||
- **Hors pics/creux** (chip "Pics / creux") : la série passe
|
||||
@ -660,7 +661,11 @@ Emplacement : `app/src/test/java/com/hormonetrack/`. Répertoire de travail d'ex
|
||||
médiane (0,5/0,9/1,4 → 0,9) ; calibration nulle sans labs ; **calibration T récupère un
|
||||
k planté (0,25)** ; grille horaire clampée à la 1ʳᵉ dose ; vide sans doses ; override
|
||||
d'ester par dose (EV≫EU à 45 h) ; prochain rappel dans le futur ; levelAt combiné
|
||||
- **`BackupGsonTest`** (1) : round-trip JSON complet (enums, IDs, notes, TConfig)
|
||||
- **`AutoBackupRetentionTest`** (5) : v1.7.0 — rétention des auto-backups :
|
||||
garde les N plus récents, ne touche JAMAIS exports manuels/logs/étrangers,
|
||||
noms malformés jamais supprimés, keep clampé ≥ 1.
|
||||
- **`BackupGsonTest`** (3) : round-trip JSON complet (enums, IDs, notes, TConfig),
|
||||
rétrocompatibilité v1 (backup sans settings), import des paramètres utilisateur
|
||||
- **`RegressionUserCaseTest`** (6) : **régression épinglée sur les données réelles
|
||||
exportées** par l'utilisatrice (backup JSON v1.0.0 : 1 traitement EEn/ESE 5 mg, 1 dose,
|
||||
4 labs dont T en ng/dL). Vérifie : parsing du JSON réel, courbes non vides et
|
||||
@ -668,7 +673,7 @@ Emplacement : `app/src/test/java/com/hormonetrack/`. Répertoire de travail d'ex
|
||||
labs antérieurs à la 1ʳᵉ dose ignorés par la calibration SF, conversion ng/dL→ng/mL,
|
||||
calibration T avec labs en ng/dL. **En cas de nouveau bug remonté par l'utilisatrice :
|
||||
exporter le JSON, l'épingler ici, reproduire, corriger.**
|
||||
- **`V120FeaturesTest`** (10) : v1.2.0→v1.2.3 — doses prévisionnelles (rythme 7 j depuis la
|
||||
- **`V120FeaturesTest`** (12) : v1.2.0→v1.2.7 — doses prévisionnelles (rythme 7 j depuis la
|
||||
dernière dose réelle, liste exacte J+4/J+11/J+18/J+25 ; vide sans intervalle ou sans
|
||||
doses ; ester override projeté), override de modèle (ESE ≠ TFS à 45 h pour EV ;
|
||||
sans override = modèle du traitement), auto-calibration v1.2.1 (facteur **par ester**
|
||||
@ -692,9 +697,144 @@ Emplacement : `app/src/test/java/com/hormonetrack/`. Répertoire de travail d'ex
|
||||
k T par ester, continuité de la courbe pendant la transition, niveau
|
||||
actuel ~EEn équilibre.
|
||||
|
||||
- **`LabsGroupingTest`** (9) : v1.2.2 — regroupement de l'écran Analyses (paire E2+T
|
||||
même timestamp ; timestamps différents séparés ; tri E2 avant T avant autres ;
|
||||
ordre chronologique décroissant). v1.7.1 — **notes d'un groupe**
|
||||
(`labNotesForDisplay`) : notes distinctes E2/T → DEUX lignes préfixées du
|
||||
marqueur (fix « une seule note sur deux »), note identique → une seule
|
||||
ligne (dédupliquée), note seule → brute (v1.0 préservé), vides/blanches
|
||||
ignorées, aucune note → aucune ligne.
|
||||
- **`ChangelogHelperTest`** (8) : v1.3.0 — comparaison SemVer **numérique**
|
||||
(1.2.9 < 1.2.10 : la comparaison lexicographique serait fausse), extraction
|
||||
de sections depuis la dernière vue, première installation (section courante
|
||||
seule), plusieurs versions intermédiaires, versions non numériques.
|
||||
- **`AppLogTest`** (4) : v1.3.1 — formatage des lignes horodatées, buffer
|
||||
circulaire (600 → 500 dernières, bordures).
|
||||
- **`HrtDurationTest`** (6) : v1.3.1 — jours depuis la 1re prise (0 si
|
||||
futur/invalide), décomposition mois(30 j)/jours.
|
||||
- **`ExportFileNamesTest`** (7) : v1.3.4/v1.7.0 — noms d'export (logs/backup),
|
||||
règle anti-#48 (pattern horaire ⇒ LocalDateTime), auto-backup horodaté +
|
||||
`parseAutoBackupTimestamp` (round-trip, non-auto-backups → null).
|
||||
- **`CalendarRruleTest`** (3) : v1.3.0 — RRULE `FREQ=DAILY;INTERVAL=N`,
|
||||
arrondi demi-supérieur des décimales (6,5 → 7), clamp ≥ 1 jour.
|
||||
- **`ExtremaTest`** (6) : v1.2.3 — détection des pics/creux (`detectExtrema`) :
|
||||
alternance stricte pic/creux en régime d'équilibre (4 doses hebdo → ≥ 4 extrema,
|
||||
pic > creux voisin, valeurs dans les bornes), courbe monotone → vide, série plate →
|
||||
vide, série trop courte → vide, **filtre d'amplitude** (sémantique zigzag : une
|
||||
oscillation sous le seuil produit UN pivot, l'alternance complète apparaît quand le
|
||||
seuil baisse), anti-corrélation E2/T (un pic d'E2 ≈ un creux de T).
|
||||
- **`TransfemScienceModelsTest`** (5) : v1.4.0 — épingle le modèle TFS V3C sur
|
||||
l'article : pics/t½ des Tableaux 9–10 (±1–2 %), AUC par mg (±3 %), état
|
||||
d'équilibre EV 5 mg/7 j = Figure 11 (Cmax 384/Cmin 142/Cavg 269), esters
|
||||
sans modèle → 0, dt ≤ 0 → 0. **Toute retouche de TransfemScienceModels.kt
|
||||
passe par CE test** (c'est la fidélité au simulateur TFS qui est vérifiée).
|
||||
- **`WhsahModelsTest`** (6) : v1.4.6 — fidélité au comportement **Mona**
|
||||
(fit WHSAH) : pics EV 367,5 @ 1,69 j / EEn 187,9 @ 4,99 j (±2 %), t½ par
|
||||
ester, linéarité par mg, PEP non couvert, `choicesForModel` ×3 modèles.
|
||||
- **`WhsahEngineTest`** (6) : v1.4.6→v1.4.8 — dispatch WHSAH (override
|
||||
depuis un traitement ESE, `pkModel = WHS`, coupure 10 × t½, PEP → 0) et
|
||||
**3 courbes EV distinctes** (ESE/TFS/WHS — seuil 2 % : ESE et TFS sont
|
||||
quasi identiques au pic par construction) ; + v1.4.8 **calibration PAR
|
||||
MODÈLE** (fix #60) : chaque courbe calibrée avec la prédiction de SON
|
||||
modèle passe par le lab (±2 %), échelles TFS ≠ WHS au même lab ; +
|
||||
v1.4.7 **calibration universelle épinglée** (le facteur par ester
|
||||
s'applique à TOUTES les courbes, indépendamment du modelOverride).
|
||||
- **`ChartZoomTest`** (23) : v1.2.9 → v1.4.10 — pas d'échantillonnage
|
||||
adaptatif, horizon + clamp de panoramique, `pointHoursBefore`,
|
||||
extension de prévision, `xLabelTicks` (minuit LOCAL, fuseau CHOISI,
|
||||
cas dégénérés), `defaultModelToggles` + **pan en cumul fractionnaire**
|
||||
(`panDeltaHours`, fix #62 : les deltas de doigt < 1 h s'accumulent
|
||||
jusqu'à franchir une heure entière — la vue 24 h ne bougeait jamais ;
|
||||
deltas négatifs = troncature vers zéro, cas dégénérés).
|
||||
- **`ScaleFactorWhsahReproTest`** (4) : v1.4.9 — épinglé sur le bug ×2,21
|
||||
(#61) : facteur manuel WHSAH 0,55 (plus jamais 2,21), cohérence
|
||||
auto↔manuel (±15 %), garde de significativité (séquence réelle 184→1 et
|
||||
cas aux frontières).
|
||||
- **`CalibrationPerModelTest`** (5) : v1.4.8 — **process calibration** :
|
||||
les 3 modèles (ESE inclus) calibrés atterrissent sur le lab ; échelles
|
||||
distinctes (ESE↔TFS ~2 %, WHS ~11 % — nuance documentée : la table ODS
|
||||
dérive du même article) ; **délégation** `autoCalibrated(modelOverride)`
|
||||
→ `computeEsterScaleFactors` (implémentation unique — Home et graphique
|
||||
ne peuvent pas diverger) ; **k T par modèle** (k ∝ 1/E2 calibrée :
|
||||
échelle ×2 → k ÷2 ; les k post-calibration convergent entre modèles —
|
||||
même E2 cible) ; labs antérieurs à la 1ʳᵉ dose → échelles vides (par
|
||||
modèle). Toute retouche du pipeline de calibration passe par CE test.
|
||||
- **`ChartScreenSourceGuardTest`** (1) : v1.4.4 — **garde de SOURCE** contre
|
||||
le bug #54 : vérifie que les vals de fenêtre du graphique sont exposées au
|
||||
gestionnaire de gestes via `rememberUpdatedState` (alias `gesture*`) et que
|
||||
la closure `detectTransformGestures` ne les lit jamais en direct (captures
|
||||
figées). Le lint Compose ne détecte pas ce pattern — ce test est le filet.
|
||||
- **`RegressionUserCase4Test`** (5) : v1.4.3/v1.4.5 — 4ᵉ régression épinglée sur
|
||||
les données réelles (export v1.4.2, HORS dépôt) : parsing v2 avec
|
||||
**settings** (seuils + auto-cal), scénario transition EV inactif → EEn
|
||||
actif, **signature pharmacocinétique EEn** (contribution d'une dose à
|
||||
J+1 < 12 % du total, plateau 48 h ≤ 25 % — c'est la réponse épinglée à
|
||||
« pourquoi l'estimation ne remonte pas le lendemain »), pas de fausse
|
||||
alerte à l'observation. **Tout nouvel export = une régression data-driven.**
|
||||
- **`RegressionUserCase5Test`** (7) : v1.7.1 — 5ᵉ régression épinglée sur
|
||||
les données réelles (export v1.7.0, HORS dépôt) : profil à 3 traitements
|
||||
(CPA oral Bateman + EEn actif + EV inactif simulé, §6.bis), **le fix
|
||||
« une note sur deux » épinglé sur la VRAIE paire** (dernière prise E2+T
|
||||
avec deux notes distinctes → `labNotesForDisplay` en rend deux, préfixées ;
|
||||
note identique → une seule), notes de DOSES préservées, plausibilité
|
||||
moteur (niveau physiologique, courbe 30 j, auto-calibration couvre un
|
||||
ester), prévision 7 j. **Tout nouvel export = une régression data-driven.**
|
||||
- **`AlertsTest`** (12) : v1.4.2 — logique pure des seuils (`pk/Alerts.kt`) :
|
||||
HIGH/LOW, comparaison STRICTE (valeur == limite → rien, éviter le
|
||||
clignotement d'arrondi), seuil null → jamais d'alerte, cohérence haut > bas,
|
||||
HIGH prime LOW en config incohérente (défensif), les deux marqueurs à la
|
||||
fois (E2 puis T), **codec d'état** (`encodeState`/`parseState` — rétro-résistant
|
||||
aux entrées malformées) et **décision de notification** (`shouldNotify` :
|
||||
nouveau franchissement → notif, même état → pas de spam, retour à la
|
||||
normale → pas de notif mais ré-arme).
|
||||
- **`AlertsEngineTest`** (4) : v1.4.2 — câblage moteur → seuils : le niveau
|
||||
évalué par la NOTIFICATION (`levelAt(scalePerEster)`) est celui de la
|
||||
CARTE accueil (`computeCurve` dernier point, ±10 % de dérive horaire) ;
|
||||
seuil à la moitié du niveau réel → HIGH, seuil au double → rien ; T bas
|
||||
→ LOW en ng/mL ; l'état persisté ne contient que les marqueurs en alerte.
|
||||
- **`TransfemScienceEngineTest`** (4) : v1.4.0 — câblage MOTEUR ↔ V3C :
|
||||
EB (sans table ODS) routé en TFS → pic article 971 pg/mL @ 0,65 j ;
|
||||
EB forcé en ESE (`modelOverride`) → 0 partout (comportement documenté :
|
||||
presets EB/EC/ECS/PEP en TFS uniquement) ; PEP 32,5 mg → contribution
|
||||
à 10 j ET 100 j (l'ancienne coupure 24 h du fallback table aurait donné 0)
|
||||
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
|
||||
du dispatch `concentrationOfDose` + de `cutoffHours`.
|
||||
- **`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
|
||||
choisie, PAS demain), heure déjà passée le jour du créneau → créneau
|
||||
suivant, créneau manqué sauté (jamais dans le passé), fallback quotidien
|
||||
sans Posologie ou sans doses, agrégat multi-traitements (le plus tôt
|
||||
gagne), traitements inactifs/désactivés → null.
|
||||
|
||||
- **`LabTimingTest`** (11) : v1.8.0/v1.8.1 — recommandation de prochaine
|
||||
prise de sang : creux juste avant le créneau, saut au premier creux
|
||||
STABILISÉ (5 × t½ ; régime non stabilisé → creux suivant), prise récente →
|
||||
creux suivant, sans Posologie/oral → null, invite Posologie, Estrannaise
|
||||
lit sa t½ dans la table ; v1.8.1 — changements récents (dose/intervalle/
|
||||
ester) réinitialisent la stabilisation.
|
||||
- **`LabTrajectoryModelTest`** (19) : v1.5.0 — « Tracé labs » (§7.10) :
|
||||
passage EXACT sur chaque lab (fp 1e-6), ρ log-linéaire (fonction pure +
|
||||
courbe recomposée via le moteur — M recalcule et ρ = geometric mean),
|
||||
1 lab → vide, **garde #61** (lab tardif PAS un ancrage + fenêtre bornée
|
||||
au dernier lab SIGNIFICATIF), labs avant 1ʳᵉ dose → 0 ancrage, fenêtre
|
||||
clippée [labs ∩ demande], **indépendance du scaleFactor stocké**
|
||||
(sinon double correction → courbes identiques sous SF 1,0 vs 0,55),
|
||||
`modelOverride` (ESE vs WHS — divergence franche : ESE≈TFS quasi
|
||||
identiques par CONSTRUCTION, seuil 2 % doc §8), labs T ignorés,
|
||||
doublons de timestamp (2ᵉ ratio gagne), bornes défensives ratioAt.
|
||||
v1.6.0 — **prolongation** (§7.10.bis) : identité `M × ρ_last` exacte
|
||||
au-delà du dernier lab (10/20/30 j), suture exacte au dernier ancre,
|
||||
horizon = cutoff du traitement (dernier point ≤ cutoff < dernier point + pas),
|
||||
dose EV loguée après le dernier lab → pic de prolongation (> ×1,3 le
|
||||
niveau pré-injection — la forme suit dosage + ester), flag off =
|
||||
v1.5.0 bit-compatible, garde #61 sur la prolongation (le lab tardif ne
|
||||
prolonge ni n'ancre), gardes du helper d'horizon (pas de dose / modèle
|
||||
éteint → null), `lastAnchorMs` (null si < 2 ancres).
|
||||
|
||||
### 8.bis Données de test réelles : HORS dépôt (`local-test-data/`)
|
||||
|
||||
Les deux classes de régression (`RegressionUserCase{,2}Test`) épinglent le comportement
|
||||
Les cinq classes de régression (`RegressionUserCase{,2,3,4,5}Test`) épinglent le comportement
|
||||
du moteur sur les **exports réels** de l'utilisatrice. Ce sont des **données de santé
|
||||
personnelles** : elles ne sont **pas versionnées**, pour ne rien divulguer dans le
|
||||
dépôt (ni maintenant, ni si le repo devient public un jour).
|
||||
@ -748,138 +888,6 @@ dépôt (ni maintenant, ni si le repo devient public un jour).
|
||||
répliquées sur les deux Gitea, aucune fuite nouvelle par push) ; la dette
|
||||
(réécriture d'historique + re-tags + force-push des 2 remotes + re-publish des
|
||||
releases) reste ouverte et doit être re-décisionnée si le dépôt passe PUBLIC.
|
||||
- **`LabsGroupingTest`** (9) : v1.2.2 — regroupement de l'écran Analyses (paire E2+T
|
||||
même timestamp ; timestamps différents séparés ; tri E2 avant T avant autres ;
|
||||
ordre chronologique décroissant). v1.7.1 — **notes d'un groupe**
|
||||
(`labNotesForDisplay`) : notes distinctes E2/T → DEUX lignes préfixées du
|
||||
marqueur (fix « une seule note sur deux »), note identique → une seule
|
||||
ligne (dédupliquée), note seule → brute (v1.0 préservé), vides/blanches
|
||||
ignorées, aucune note → aucune ligne.
|
||||
- **`ChangelogHelperTest`** (8) : v1.3.0 — comparaison SemVer **numérique**
|
||||
(1.2.9 < 1.2.10 : la comparaison lexicographique serait fausse), extraction
|
||||
de sections depuis la dernière vue, première installation (section courante
|
||||
seule), plusieurs versions intermédiaires, versions non numériques.
|
||||
- **`AppLogTest`** (4) : v1.3.1 — formatage des lignes horodatées, buffer
|
||||
circulaire (600 → 500 dernières, bordures).
|
||||
- **`HrtDurationTest`** (6) : v1.3.1 — jours depuis la 1re prise (0 si
|
||||
futur/invalide), décomposition mois(30 j)/jours.
|
||||
- **`CalendarRruleTest`** (3) : v1.3.0 — RRULE `FREQ=DAILY;INTERVAL=N`,
|
||||
arrondi demi-supérieur des décimales (6,5 → 7), clamp ≥ 1 jour.
|
||||
- **`ExtremaTest`** (6) : v1.2.3 — détection des pics/creux (`detectExtrema`) :
|
||||
alternance stricte pic/creux en régime d'équilibre (4 doses hebdo → ≥ 4 extrema,
|
||||
pic > creux voisin, valeurs dans les bornes), courbe monotone → vide, série plate →
|
||||
vide, série trop courte → vide, **filtre d'amplitude** (sémantique zigzag : une
|
||||
oscillation sous le seuil produit UN pivot, l'alternance complète apparaît quand le
|
||||
seuil baisse), anti-corrélation E2/T (un pic d'E2 ≈ un creux de T).
|
||||
- **`TransfemScienceModelsTest`** (5) : v1.4.0 — épingle le modèle TFS V3C sur
|
||||
l'article : pics/t½ des Tableaux 9–10 (±1–2 %), AUC par mg (±3 %), état
|
||||
d'équilibre EV 5 mg/7 j = Figure 11 (Cmax 384/Cmin 142/Cavg 269), esters
|
||||
sans modèle → 0, dt ≤ 0 → 0. **Toute retouche de TransfemScienceModels.kt
|
||||
passe par CE test** (c'est la fidélité au simulateur TFS qui est vérifiée).
|
||||
- **`WhsahModelsTest`** (6) : v1.4.6 — fidélité au comportement **Mona**
|
||||
(fit WHSAH) : pics EV 367,5 @ 1,69 j / EEn 187,9 @ 4,99 j (±2 %), t½ par
|
||||
ester, linéarité par mg, PEP non couvert, `choicesForModel` ×3 modèles.
|
||||
- **`WhsahEngineTest`** (5) : v1.4.6/v1.4.7 — dispatch WHSAH (override depuis un
|
||||
identiques au pic par construction) ; + **calibration universelle épinglée**
|
||||
(le facteur par ester s'applique à TOUTES les courbes, indépendamment du
|
||||
modelOverride — demande v1.4.7).
|
||||
traitement ESE, `pkModel = WHS`, coupure 10 × t½, PEP → 0) et **3 courbes
|
||||
EV distinctes** (ESE/TFS/WHS — seuil 2 % : ESE et TFS sont quasi
|
||||
identiques au pic par construction).
|
||||
- **`WhsahEngineTest`** (6) : + v1.4.8 — **calibration PAR MODÈLE** (fix
|
||||
#60) : chaque courbe calibrée avec la prédiction de SON modèle passe par
|
||||
le lab (±2 %) ; échelles TFS ≠ WHS au même lab.
|
||||
- **`ChartZoomTest`** (19) : + v1.4.10 — **pan en cumul fractionnaire**
|
||||
(`panDeltaHours`, fix #62 : les deltas de doigt < 1 h s'accumulent
|
||||
jusqu'à franchir une heure entière — la vue 24 h ne bougeait jamais ;
|
||||
deltas négatifs = troncature vers zéro, cas dégénérés).
|
||||
- **`ScaleFactorWhsahReproTest`** (4) : v1.4.9 — épinglé sur le bug ×2,21
|
||||
(#61) : facteur manuel WHSAH 0,55 (plus jamais 2,21), cohérence
|
||||
auto↔manuel (±15 %), garde de significativité (séquence réelle 184→1 et
|
||||
cas aux frontières).
|
||||
- **`CalibrationPerModelTest`** (5) : v1.4.8 — **process calibration** :
|
||||
les 3 modèles (ESE inclus) calibrés atterrissent sur le lab ; échelles
|
||||
distinctes (ESE↔TFS ~2 %, WHS ~11 % — nuance documentée : la table ODS
|
||||
dérive du même article) ; **délégation** `autoCalibrated(modelOverride)`
|
||||
→ `computeEsterScaleFactors` (implémentation unique — Home et graphique
|
||||
ne peuvent pas diverger) ; **k T par modèle** (k ∝ 1/E2 calibrée :
|
||||
échelle ×2 → k ÷2 ; les k post-calibration convergent entre modèles —
|
||||
même E2 cible) ; labs antérieurs à la 1ʳᵉ dose → échelles vides (par
|
||||
modèle). Toute retouche du pipeline de calibration passe par CE test.
|
||||
- **`ChartZoomTest`** (15) : v1.2.9 (pas d'échantillonnage adaptatif) /
|
||||
v1.4.1 (horizon + clamp panoramique) / v1.4.5 (`pointHoursBefore`,
|
||||
`xLabelTicks` minuit local) / v1.4.7 — **`defaultModelToggles`** (toggles
|
||||
alignés sur les modèles utilisés par les traitements ; garde-fou TFS si
|
||||
aucun profil),
|
||||
horizon + clamp de panoramique, `pointHoursBefore`, extension de prévision,
|
||||
**`xLabelTicks`** (minuit LOCAL, fuseau CHOISI America/New_York, heures
|
||||
rondes, cas dégénérés).
|
||||
- **`ChartScreenSourceGuardTest`** (1) : v1.4.4 — **garde de SOURCE** contre
|
||||
le bug #54 : vérifie que les vals de fenêtre du graphique sont exposées au
|
||||
gestionnaire de gestes via `rememberUpdatedState` (alias `gesture*`) et que
|
||||
la closure `detectTransformGestures` ne les lit jamais en direct (captures
|
||||
figées). Le lint Compose ne détecte pas ce pattern — ce test est le filet.
|
||||
- **`RegressionUserCase4Test`** (4) : v1.4.3 — 4ᵉ régression épinglée sur
|
||||
les données réelles (export v1.4.2, HORS dépôt) : parsing v2 avec
|
||||
**settings** (seuils + auto-cal), scénario transition EV inactif → EEn
|
||||
actif, **signature pharmacocinétique EEn** (contribution d'une dose à
|
||||
J+1 < 12 % du total, plateau 48 h ≤ 25 % — c'est la réponse épinglée à
|
||||
« pourquoi l'estimation ne remonte pas le lendemain »), pas de fausse
|
||||
alerte à l'observation. **Tout nouvel export = une régression data-driven.**
|
||||
- **`RegressionUserCase5Test`** (7) : v1.7.1 — 5ᵉ régression épinglée sur
|
||||
les données réelles (export v1.7.0, HORS dépôt) : profil à 3 traitements
|
||||
(CPA oral Bateman + EEn actif + EV inactif simulé, §6.bis), **le fix
|
||||
« une note sur deux » épinglé sur la VRAIE paire** (dernière prise E2+T
|
||||
avec deux notes distinctes → `labNotesForDisplay` en rend deux, préfixées ;
|
||||
note identique → une seule), notes de DOSES préservées, plausibilité
|
||||
moteur (niveau physiologique, courbe 30 j, auto-calibration couvre un
|
||||
ester), prévision 7 j. **Tout nouvel export = une régression data-driven.**
|
||||
- **`AlertsTest`** (12) : v1.4.2 — logique pure des seuils (`pk/Alerts.kt`) :
|
||||
HIGH/LOW, comparaison STRICTE (valeur == limite → rien, éviter le
|
||||
clignotement d'arrondi), seuil null → jamais d'alerte, cohérence haut > bas,
|
||||
HIGH prime LOW en config incohérente (défensif), les deux marqueurs à la
|
||||
fois (E2 puis T), **codec d'état** (`encodeState`/`parseState` — rétro-résistant
|
||||
aux entrées malformées) et **décision de notification** (`shouldNotify` :
|
||||
nouveau franchissement → notif, même état → pas de spam, retour à la
|
||||
normale → pas de notif mais ré-arme).
|
||||
- **`AlertsEngineTest`** (4) : v1.4.2 — câblage moteur → seuils : le niveau
|
||||
évalué par la NOTIFICATION (`levelAt(scalePerEster)`) est celui de la
|
||||
CARTE accueil (`computeCurve` dernier point, ±10 % de dérive horaire) ;
|
||||
seuil à la moitié du niveau réel → HIGH, seuil au double → rien ; T bas
|
||||
→ LOW en ng/mL ; l'état persisté ne contient que les marqueurs en alerte.
|
||||
- **`TransfemScienceEngineTest`** (4) : v1.4.0 — câblage MOTEUR ↔ V3C :
|
||||
EB (sans table ODS) routé en TFS → pic article 971 pg/mL @ 0,65 j ;
|
||||
EB forcé en ESE (`modelOverride`) → 0 partout (comportement documenté :
|
||||
presets EB/EC/ECS/PEP en TFS uniquement) ; PEP 32,5 mg → contribution
|
||||
à 10 j ET 100 j (l'ancienne coupure 24 h du fallback table aurait donné 0)
|
||||
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
|
||||
du dispatch `concentrationOfDose` + de `cutoffHours`.
|
||||
- **`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
|
||||
choisie, PAS demain), heure déjà passée le jour du créneau → créneau
|
||||
suivant, créneau manqué sauté (jamais dans le passé), fallback quotidien
|
||||
sans Posologie ou sans doses, agrégat multi-traitements (le plus tôt
|
||||
gagne), traitements inactifs/désactivés → null.
|
||||
|
||||
- **`LabTrajectoryModelTest`** (19) : v1.5.0 — « Tracé labs » (§7.10) :
|
||||
passage EXACT sur chaque lab (fp 1e-6), ρ log-linéaire (fonction pure +
|
||||
courbe recomposée via le moteur — M recalcule et ρ = geometric mean),
|
||||
1 lab → vide, **garde #61** (lab tardif PAS un ancrage + fenêtre bornée
|
||||
au dernier lab SIGNIFICATIF), labs avant 1ʳᵉ dose → 0 ancrage, fenêtre
|
||||
clippée [labs ∩ demande], **indépendance du scaleFactor stocké**
|
||||
(sinon double correction → courbes identiques sous SF 1,0 vs 0,55),
|
||||
`modelOverride` (ESE vs WHS — divergence franche : ESE≈TFS quasi
|
||||
identiques par CONSTRUCTION, seuil 2 % doc §8), labs T ignorés,
|
||||
doublons de timestamp (2ᵉ ratio gagne), bornes défensives ratioAt.
|
||||
v1.6.0 — **prolongation** (§7.10.bis) : identité `M × ρ_last` exacte
|
||||
au-delà du dernier lab (10/20/30 j), suture exacte au dernier ancre,
|
||||
horizon = cutoff du traitement (dernier point ≤ cutoff < dernier point + pas),
|
||||
dose EV loguée après le dernier lab → pic de prolongation (> ×1,3 le
|
||||
niveau pré-injection — la forme suit dosage + ester), flag off =
|
||||
v1.5.0 bit-compatible, garde #61 sur la prolongation (le lab tardif ne
|
||||
prolonge ni n'ancre), gardes du helper d'horizon (pas de dose / modèle
|
||||
éteint → null), `lastAnchorMs` (null si < 2 ancres).
|
||||
|
||||
**Ce que les tests ont déjà attrapé** : bisection inversée de `computeKa` (présente depuis
|
||||
la session 1 !), plancher 0,01 des queues de profils, mapping silencieux du modèle inconnu.
|
||||
@ -1388,7 +1396,7 @@ aucun contexte, dans l'esprit de cette doc) :**
|
||||
JSON marchait car `yyyyMMdd` est un pattern valide pour LocalDate.
|
||||
Zones d'ombre emboîtées : v1.3.2 et v1.3.3 « corrigent » l'IO (cf #41/#45)
|
||||
sans voir cette ligne. FIX : `util/ExportFileNames.kt` (pur, testé,
|
||||
documenté — couplage type↔pattern centralisé) ; `FileNamesTest` épinglant
|
||||
documenté — couplage type↔pattern centralisé) ; `ExportFileNamesTest` épinglant
|
||||
les deux helpers (aurait attrapé le bug le jour même). Leçon : la
|
||||
génération de nom de fichier ne doit JAMAIS vivre inline dans un onClick.
|
||||
49. **StringFormatMatches** (capture du lint, v1.3.4) : la notification de
|
||||
@ -1602,7 +1610,7 @@ l'optimiseur.
|
||||
le chip « Lab track » s'active MAIS la courbe n'est ni dessinée (0 pixel
|
||||
#C2185B au screencap) ni légendée ; « Prolonger » idem. En DEBUG, le même
|
||||
chip sur les mêmes données fonctionne (légende + 7090 px rose après fix).
|
||||
**Les 191 tests JVM sont verts** (ils ne passent JAMAIS par R8).
|
||||
**Les 191 tests JVM de l'époque sont verts** (223 aujourd'hui — ils ne passent JAMAIS par R8).
|
||||
|
||||
**Diagnostic** (méthode de bisection debug/release puis `mapping.txt`) :
|
||||
`com.hormonetrack.pk.LabTrajectoryModel -> R8$$REMOVED$$CLASS$$…` — R8 full
|
||||
@ -1617,6 +1625,13 @@ proguard-rules.pro (garde commentée + renvoi au mapping). Validé émulateur
|
||||
sur l'APK release re-buildé : ancrée (courbe + légende), prolongée (légende
|
||||
+ avertissement), 0 crash.
|
||||
|
||||
**#65/#66 (v1.8.2) — courbes d'accueil/graphiques pas rafraîchies sur édition** : les
|
||||
clés des `produceState` étaient incomplètes (Home : `allTreatments`/`tConfig` absentes ;
|
||||
Chart : `labResults.size` au lieu de la liste) — éditer un scaleFactor/le modèle T/la
|
||||
valeur d'un lab ne rafraîchissait pas les courbes avant le tick de 60 s. Leçon :
|
||||
les clés d'un producteur d'état Compose doivent couvrir TOUTE entrée du calcul (pas
|
||||
un proxy comme `.size`).
|
||||
|
||||
## 15. Comment régénérer l'asset pk_profiles.json
|
||||
|
||||
Si le `.ods` change (re-fits, nouveaux esters) :
|
||||
@ -1641,7 +1656,7 @@ structure ci-dessus. **Toute restructuration du JSON impose de mettre à jour
|
||||
|
||||
```bash
|
||||
cd ~/projects/HormoneTrack
|
||||
./gradlew assembleDebug testDebugUnitTest # build + 87 tests (44 sans données locales)
|
||||
./gradlew assembleDebug testDebugUnitTest # build + 223 tests (193 sans données locales)
|
||||
./gradlew assembleRelease # APK optimisé R8 (cf §16.bis)
|
||||
adb install -r app/build/outputs/apk/debug/app-debug.apk
|
||||
```
|
||||
@ -1709,8 +1724,9 @@ Le script :
|
||||
(`[1.2.3]`) — première version du script cherchait `[vX.Y.Z]` et tombait sur
|
||||
le fallback « Voir docs/CHANGELOG.md » ;
|
||||
- crée la release si absente, sinon **met à jour le corps** (PATCH) ;
|
||||
- attache l'APK (remplace l'asset du même nom si présent) — appeler le script
|
||||
DEUX FOIS pour publier les deux APK (noms distincts) ;
|
||||
- attache l'APK (remplace l'asset du même nom si présent) — ⚠️ script
|
||||
mono-APK : ne plus l'utiliser pour publier les APK, cf #43 et
|
||||
publish-release.py (UNE invocation fait tout) ;
|
||||
⚠️ vérification intégrée (`ensure_asset`) : le `?name=` de l'upload et/ou
|
||||
le PATCH peuvent être ignorés par les instances (assets au nom générique /
|
||||
APK disparu — v1.2.6, v1.2.10) → le script vérifie **nom ET taille** après
|
||||
@ -1740,9 +1756,9 @@ vérifie pas en tests JVM).
|
||||
|
||||
1. **Bumper la version** dans `app/build.gradle.kts` : `versionCode = N+1`,
|
||||
`versionName = "X.Y.Z+1"` (SemVer : fix = Z, feature = Y).
|
||||
2. **Tests verts obligatoires** : `./gradlew testDebugUnitTest` — 191 au
|
||||
total, 168 si `local-test-data/` est absent (les 4 classes de régression
|
||||
réelles sont skippées via `Assume`, 6/6/6/5 tests) ; **lint vert
|
||||
2. **Tests verts obligatoires** : `./gradlew testDebugUnitTest` — 223 au
|
||||
total, 193 si `local-test-data/` est absent (les 5 classes de régression
|
||||
réelles sont skippées via `Assume`, 6/6/6/5/7 tests) ; **lint vert
|
||||
obligatoire** : `./gradlew lint` (v1.3.4 — `StringFormatMatches` aurait
|
||||
attrapé les crashs #47/#48 dès v1.3.1). Paliers de test du projet :
|
||||
chaque commit = tests + lint ; release = + scan confidentialité + APK au
|
||||
@ -1756,7 +1772,7 @@ vérifie pas en tests JVM).
|
||||
headless hrt36 + seed `scripts/seed-emulator.py` avec le dernier
|
||||
backup de `local-test-data/` + APK release installé PAR-DESSUS) sur
|
||||
**l'APK RELEASE** (JAMAIS le debug seul : **R8 ne se vérifie qu'ici** —
|
||||
les 191 tests JVM ne le voient jamais, cf #64). Parcours minimum :
|
||||
les 223 tests JVM ne le voient jamais, cf #64). Parcours minimum :
|
||||
l'app démarre 0 crash → écran Graphiques → activer les chips de la
|
||||
release (rangée modèles défilable : `adb shell input swipe 900 716 150
|
||||
716 400` puis tap aux bounds du `uiautomator dump`) → vérifier dans le
|
||||
@ -1775,7 +1791,7 @@ vérifie pas en tests JVM).
|
||||
`Siphonight/HormoneTrack-web` — historique 100 % propre, séparé de la
|
||||
dette de confidentialité de ce dépôt). À chaque release : porter la
|
||||
feature web si pas déjà fait, aligner `WEB_VERSION` sur la MÊME `X.Y.Z`,
|
||||
`bash scripts/check.sh` vert (121 tests + E2E), commit + tag `vX.Y.Z`
|
||||
`bash scripts/check.sh` vert (155 tests + E2E), commit + tag `vX.Y.Z`
|
||||
côté web, push des deux dépôts. Changelogs TOTALEMENT SÉPARÉS
|
||||
(`web/docs/CHANGELOG.md` ≠ ce fichier). Process web complet :
|
||||
`../HormoneTrack-web/docs/DEVELOPPEMENT.md` §3 + §10. Publication
|
||||
@ -1874,8 +1890,7 @@ signal ; stack : `adb logcat -b crash -d` (grep FATAL). **0 crash attendu :
|
||||
`adb shell input swipe` ne délivrent PAS les mouvements intermédiaires au
|
||||
chart (`detectTransformGestures` ne reçoit que down/up sans moves) → le
|
||||
pan et le pinch sont INTESTABLES par adb sur cet environnement (bug #62
|
||||
non détectable ici). Le pan/pinch se valide sur le Pixel 9 réel (l'appa-
|
||||
rel du test humain). Les TAPS, la navigation et les dialogs fonctionnent
|
||||
non détectable ici). Le pan/pinch se valide sur le Pixel 9 réel (l'appareil du test humain). Les TAPS, la navigation et les dialogs fonctionnent
|
||||
normalement par adb.
|
||||
|
||||
**5. Flux SAF (export JSON/logs)** : sur android-31 ATD le
|
||||
@ -1931,7 +1946,7 @@ Volontaires (v1) :
|
||||
- DST : les rappels quotidiens peuvent glisser d'1 h après changement d'heure, jusqu'au
|
||||
prochain reschedule (boot/save) — mineur
|
||||
- Labs : marqueur libre — E2/T exacts requis pour calibration/charts
|
||||
- `allowBackup=false` → seul backup = export JSON manuel
|
||||
- `allowBackup=false` → backups = export JSON manuel + auto-backup quotidien (v1.7.0, dans le dossier choisi)
|
||||
- **APK release (R8)** : la réflexion Gson est couverte par des `-keep`
|
||||
explicites, mais R8 ne se vérifie pas en tests JVM → **smoke-test sur
|
||||
téléphone** (export/import backup, graphiques) avant chaque publication ;
|
||||
@ -1994,15 +2009,15 @@ Sur le téléphone de test (à compléter par l'utilisatrice) :
|
||||
- [ ] **v1.2.x** : édition d'une dose (tap ligne Doses) et d'un lab (tap ligne
|
||||
Analyses → sélecteur E2/T si paire) ; suppression par prise entière
|
||||
- [ ] **v1.2.x** : prise de sang E2 + T en une entrée (champs optionnels)
|
||||
- [ ] **v1.2.9** : zoom du graphique (pinch 2 doigts + boutons − / + ; labels
|
||||
X adaptatifs ; courbes lisses à fort zoom)
|
||||
- [ ] **v1.2.7** : prévision après un OUBLI (pas de faux pic dans le passé)
|
||||
et après un RETARD (prévision décalée, suivant la dernière prise)
|
||||
- [ ] **v1.2.6** : import d'un backup avec l'app déjà remplie → ÉCRASER &
|
||||
restaurer (message clair), tConfig restauré, rappels reprogrammés
|
||||
- [ ] **v1.2.4** : passer un traitement à inactif → retiré de « Log rapide » et
|
||||
du dropdown des nouvelles doses, rappel annulé, **mais sa simulation reste
|
||||
sur le graphique** et la calibration couvre toujours ses périodes
|
||||
- [ ] **v1.2.6** : import d'un backup avec l'app déjà remplie → ÉCRASER &
|
||||
restaurer (message clair), tConfig restauré, rappels reprogrammés
|
||||
- [ ] **v1.2.7** : prévision après un OUBLI (pas de faux pic dans le passé)
|
||||
et après un RETARD (prévision décalée, suivant la dernière prise)
|
||||
- [ ] **v1.2.9** : zoom du graphique (pinch 2 doigts + boutons − / + ; labels
|
||||
X adaptatifs ; courbes lisses à fort zoom)
|
||||
- [ ] **v1.3.3** : **export des logs** → le gestionnaire de fichiers s'ouvre,
|
||||
le .txt est écrit, un message « Logs de diagnostic exportés » s'affiche ;
|
||||
cas d'échec (annulation du chooser = silencieux, échec d'écriture =
|
||||
@ -2081,6 +2096,21 @@ Sur le téléphone de test (à compléter par l'utilisatrice) :
|
||||
- [ ] **v1.4.6** : **3ᵉ modèle WHSAH** : preset WHSAH → courbe verte
|
||||
superposable aux 2 autres (montée plus rapide à J+1, t½ plus longues
|
||||
que TFS) ; PEP non couvert ; 6 presets ; dropdown modèle à 3 choix
|
||||
- [ ] **v1.4.7** : **toggles alignés sur les traitements** : au 1ᵉʳ chargement
|
||||
du graphique, seuls les modèles des traitements à profil sont ON
|
||||
(ex. EV TFS + EEn WHSAH → Estrannaise OFF) ; les autres activables au
|
||||
tap ; et TOUTES les courbes affichées reflètent la calibration (test)
|
||||
- [ ] **v1.4.8** : **calibration par modèle** : superposer TFS + WHSAH avec
|
||||
l'auto-calibration ON → CHAQUE courbe colle aux labs (plus de facteur
|
||||
partagé absurde type ×2,21 — fix #60)
|
||||
- [ ] **v1.4.9** : **calibration ×2,21 corrigée** (#61) : le bouton
|
||||
« Calibrer avec les analyses » et l'auto-calibration retombent sur un
|
||||
facteur physiologique (< 1,2) même avec des doses de test anciennes ;
|
||||
hint dans l'éditeur quand l'auto-calibration écrase le facteur manuel
|
||||
- [ ] **v1.4.10** : **pan sur la vue 24 h** : glisser horizontalement à
|
||||
24 h doit déplacer la courbe (fix #62 : les deltas < 1 h s'accumulent
|
||||
— avant, le pan ne bougeait jamais sur 24 h) ; tester aussi à 7 j et
|
||||
le drag gauche avec prévision (futur)
|
||||
- [ ] **v1.5.0** : **Tracé labs** : chip `Tracé labs` (rangée modèles,
|
||||
off par défaut) → la courbe rose foncé pointillé passe EXACTEMENT
|
||||
sur chacun de tes labs E2 entre le 1er et le dernier LAB ; le reste
|
||||
@ -2121,24 +2151,11 @@ Sur le téléphone de test (à compléter par l'utilisatrice) :
|
||||
recommandé → la carte passe au creux SUIVANT ; enlever la Posologie
|
||||
→ carte remplacée par l'invite « renseigne une Posologie » ;
|
||||
traitement oral seul → aucune carte
|
||||
- [ ] **v1.4.10** : **pan sur la vue 24 h** : glisser horizontalement à
|
||||
24 h doit déplacer la courbe (fix #62 : les deltas < 1 h s'accumulent
|
||||
— avant, le pan ne bougeait jamais sur 24 h) ; tester aussi à 7 j et
|
||||
le drag gauche avec prévision (futur)
|
||||
|
||||
- [ ] **v1.4.9** : **calibration ×2,21 corrigée** (#61) : le bouton
|
||||
« Calibrer avec les analyses » et l'auto-calibration retombent sur un
|
||||
facteur physiologique (< 1,2) même avec des doses de test anciennes ;
|
||||
hint dans l'éditeur quand l'auto-calibration écrase le facteur manuel
|
||||
|
||||
- [ ] **v1.4.8** : **calibration par modèle** : superposer TFS + WHSAH avec
|
||||
l'auto-calibration ON → CHAQUE courbe colle aux labs (plus de facteur
|
||||
partagé absurde type ×2,21 — fix #60)
|
||||
|
||||
- [ ] **v1.4.7** : **toggles alignés sur les traitements** : au 1ᵉʳ chargement
|
||||
du graphique, seuls les modèles des traitements à profil sont ON
|
||||
(ex. EV TFS + EEn WHSAH → Estrannaise OFF) ; les autres activables au
|
||||
tap ; et TOUTES les courbes affichées reflètent la calibration (test)
|
||||
- [ ] **v1.8.1** : **stabilisation corrigée + suggestion Accueil** : après
|
||||
changement de dose/ester/intervalle, la carte ne dit plus « stabilisé
|
||||
depuis » l'ancienne période (régime = séquence terminale (ester, dose,
|
||||
écart) constants) ; une carte compacte de recommandation apparaît
|
||||
aussi sur l'Accueil ; 0 crash
|
||||
|
||||
---
|
||||
*Doc mise à jour le 16 sept. 2026 (v1.7.1) — build OK, lint vert, 211/211 tests verts (181 sans
|
||||
|
||||
@ -122,7 +122,7 @@ Les notifications de l'app remontent automatiquement sur la montre via **Huawei
|
||||
- **Supprimer** : la corbeille retire la prise entière (E2 + T ensemble)
|
||||
- Choisis bien l'unité : elle est convertie automatiquement pour l'affichage et la
|
||||
calibration (E2 en pg/mL ; T en ng/mL, ng/dL, ng/L, nmol/L)
|
||||
- **Prochaine prise de sang (suggestion, v1.8.0)** : si ton traitement E2
|
||||
- **Prochaine prise de sang (suggestion, v1.8.0 ; v1.8.1 : aussi sur l'Accueil)** : si ton traitement E2
|
||||
injectable actif a une **Posologie**, la page te suggère quand faire la
|
||||
prochaine analyse — **au creux estimé, juste avant l'injection suivante**
|
||||
(moment le plus comparable), au premier creux où ton ester est stabilisé
|
||||
@ -248,7 +248,7 @@ Pics de référence (dose unique de 5 mg, IM) :
|
||||
|
||||
| Problème | Solution |
|
||||
|----------|----------|
|
||||
| « SDK not found » au sync | Android Studio → Settings → Languages & Frameworks → Android SDK → installer API 34 |
|
||||
| « SDK not found » au sync | Android Studio → Settings → Languages & Frameworks → Android SDK → vérifier/installer la plateforme 37 (le build AGP peut la télécharger automatiquement) |
|
||||
| Pas de téléphone détecté | Réactive le Débogage USB, change de câble (données, pas charge seule) |
|
||||
| Notif absente sur la montre | Huawei Santé → Notifications → autorise l'app ; redémarre la montre |
|
||||
| Rappels en retard | Paramètres → « Accorder les alarmes exactes » + désactive l'optimisation de batterie pour l'app |
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user