From 3a647c596d1e3298eab5328b2cd3ddf5241caba2 Mon Sep 17 00:00:00 2001 From: Siphonight Date: Sat, 5 Sep 2026 21:41:58 +0200 Subject: [PATCH] =?UTF-8?q?v1.2.6=20:=20valeurs=20des=20pics/creux=20sur?= =?UTF-8?q?=20le=20graphique=20+=20import=20JSON=20en=20mode=20=C3=A9crase?= =?UTF-8?q?ment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CurveChart : drawExtremum = triangle ▲▼ + VALEUR estimée de l'extremum (au-dessus d'un pic, en dessous d'un creux), couleur de la série - Import JSON : mode ÉCRASEMENT (bug remonté : échec dès que l'app contenait des données — conflit d'IDs backup vs existants) · DAOs : deleteAll() sur les 3 tables (ordre enfants → parents, FK CASCADE) · BackupManager.importJson : wipe puis restauration (IDs conservés → FK valides), retourne aussi le tConfig du backup · SettingsScreen : après import → tConfig restauré dans DataStore + rappels reprogrammés (rescheduleAll) ; dialog renommé « Effacer & restaurer » avec avertissement explicite - docs : §13 (écrasement), §11 (valeurs extrema), §14 #34, §16.bis CHECKLIST DE DÉPLOIEMENT complète numérotée (version → tests → docs → commit/tag → push 2 remotes → build 2 APK → releases 2 instances → smoke-test R8), §20/§21, CHANGELOG, README - versionCode 9, versionName 1.2.6 --- README.md | 2 +- app/build.gradle.kts | 4 +-- .../hormonetrack/data/backup/BackupManager.kt | 28 +++++++++++++-- .../com/hormonetrack/data/dao/DoseLogDao.kt | 4 +++ .../com/hormonetrack/data/dao/LabResultDao.kt | 4 +++ .../com/hormonetrack/data/dao/TreatmentDao.kt | 5 +++ .../data/repository/HormoneRepository.kt | 5 +++ .../hormonetrack/ui/components/CurveChart.kt | 36 +++++++++++++++---- .../hormonetrack/ui/screens/SettingsScreen.kt | 7 ++++ app/src/main/res/values-fr/strings.xml | 6 ++-- app/src/main/res/values/strings.xml | 6 ++-- docs/DEVELOPPEMENT.md | 33 +++++++++++++++-- 12 files changed, 121 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 6a73d00..f8e6c1b 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,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.2.5 — build Android ✅, **62 tests unitaires** ✅ (44 sans les données de test locales ; 3 régressions épinglées sur données réelles **non versionnées**), intégration montre = notifications ✅, **dépôt Gitea privé + releases avec APK** ✅ +- **Statut** : v1.2.6 — build Android ✅, **62 tests unitaires** ✅ (44 sans les données de test locales ; 3 régressions épinglées sur données réelles **non versionnées**), intégration montre = notifications ✅, **dépôt Gitea privé + releases avec APK** ✅ - **Journal des versions** : [docs/CHANGELOG.md](docs/CHANGELOG.md) - **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) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bbb82d2..00ef6d2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "com.hormonetrack" minSdk = 26 targetSdk = 36 - versionCode = 8 - versionName = "1.2.5" + versionCode = 9 + versionName = "1.2.6" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/java/com/hormonetrack/data/backup/BackupManager.kt b/app/src/main/java/com/hormonetrack/data/backup/BackupManager.kt index 12eb6bb..1443459 100644 --- a/app/src/main/java/com/hormonetrack/data/backup/BackupManager.kt +++ b/app/src/main/java/com/hormonetrack/data/backup/BackupManager.kt @@ -31,8 +31,26 @@ object BackupManager { return Gson().toJson(data) } - data class ImportResult(val treatments: Int, val doseLogs: Int, val labResults: Int) + data class ImportResult( + val treatments: Int, + val doseLogs: Int, + val labResults: Int, + /** Réglages T du backup, à restaurer dans DataStore par l'appelant. */ + val tConfig: TConfig + ) + /** + * Restaure un backup en mode **ÉCRASEMENT** (v1.2.6) : les données actuelles + * sont effacées AVANT l'insertion — sinon les IDs du backup (conservés pour + * les FK dose→traitement) entrent en conflit avec les IDs existants et + * l'import échouait dès que l'app contenait déjà des données. + * + * Ordre d'effacement : dose_logs et lab_results d'abord, treatments ensuite + * (FK CASCADE des doses vers les traitements). + * + * @return le nombre d'entrées restaurées par table + le tConfig du backup + * (à restaurer dans DataStore par l'appelant). + */ suspend fun importJson( repo: HormoneRepository, json: String @@ -40,6 +58,11 @@ object BackupManager { val type = object : TypeToken() {}.type val data: BackupData = Gson().fromJson(json, type) + // Écrasement : on part d'une base vide (ordre : enfants → parents) + repo.deleteAllDoseLogs() + repo.deleteAllLabResults() + repo.deleteAllTreatments() + data.treatments.forEach { repo.insertTreatment(it) } data.doseLogs.forEach { repo.insertDoseLog(it) } data.labResults.forEach { repo.insertLabResult(it) } @@ -47,7 +70,8 @@ object BackupManager { return ImportResult( data.treatments.size, data.doseLogs.size, - data.labResults.size + data.labResults.size, + data.tConfig ) } diff --git a/app/src/main/java/com/hormonetrack/data/dao/DoseLogDao.kt b/app/src/main/java/com/hormonetrack/data/dao/DoseLogDao.kt index 003f8dc..a25ddac 100644 --- a/app/src/main/java/com/hormonetrack/data/dao/DoseLogDao.kt +++ b/app/src/main/java/com/hormonetrack/data/dao/DoseLogDao.kt @@ -24,6 +24,10 @@ interface DoseLogDao { @Query("SELECT * FROM dose_logs ORDER BY timestamp ASC") suspend fun getAllOnce(): List + /** Efface TOUT l'historique (import en mode écrasement). */ + @Query("DELETE FROM dose_logs") + suspend fun deleteAll() + @Insert suspend fun insert(log: DoseLog): Long diff --git a/app/src/main/java/com/hormonetrack/data/dao/LabResultDao.kt b/app/src/main/java/com/hormonetrack/data/dao/LabResultDao.kt index ed0341f..4814ca4 100644 --- a/app/src/main/java/com/hormonetrack/data/dao/LabResultDao.kt +++ b/app/src/main/java/com/hormonetrack/data/dao/LabResultDao.kt @@ -18,6 +18,10 @@ interface LabResultDao { @Query("SELECT * FROM lab_results ORDER BY timestamp ASC") suspend fun getAllOnce(): List + /** Efface toutes les analyses (import en mode écrasement). */ + @Query("DELETE FROM lab_results") + suspend fun deleteAll() + @Insert suspend fun insert(result: LabResult): Long diff --git a/app/src/main/java/com/hormonetrack/data/dao/TreatmentDao.kt b/app/src/main/java/com/hormonetrack/data/dao/TreatmentDao.kt index 823a1fc..90b0de5 100644 --- a/app/src/main/java/com/hormonetrack/data/dao/TreatmentDao.kt +++ b/app/src/main/java/com/hormonetrack/data/dao/TreatmentDao.kt @@ -21,6 +21,11 @@ interface TreatmentDao { @Query("SELECT * FROM treatments ORDER BY createdAt DESC") suspend fun getAllOnce(): List + /** Efface tous les traitements (import en mode écrasement). + * ⚠️ À appeler APRÈS deleteAll() des dose_logs (FK CASCADE sinon). */ + @Query("DELETE FROM treatments") + suspend fun deleteAll() + @Insert suspend fun insert(treatment: Treatment): Long diff --git a/app/src/main/java/com/hormonetrack/data/repository/HormoneRepository.kt b/app/src/main/java/com/hormonetrack/data/repository/HormoneRepository.kt index f789651..5403447 100644 --- a/app/src/main/java/com/hormonetrack/data/repository/HormoneRepository.kt +++ b/app/src/main/java/com/hormonetrack/data/repository/HormoneRepository.kt @@ -44,4 +44,9 @@ class HormoneRepository( suspend fun insertLabResult(result: LabResult): Long = labResultDao.insert(result) suspend fun updateLabResult(result: LabResult) = labResultDao.update(result) suspend fun deleteLabResult(result: LabResult) = labResultDao.delete(result) + + // --- Effacement complet (import en mode écrasement, cf BackupManager) --- + suspend fun deleteAllDoseLogs() = doseLogDao.deleteAll() + suspend fun deleteAllLabResults() = labResultDao.deleteAll() + suspend fun deleteAllTreatments() = treatmentDao.deleteAll() } diff --git a/app/src/main/java/com/hormonetrack/ui/components/CurveChart.kt b/app/src/main/java/com/hormonetrack/ui/components/CurveChart.kt index a153d63..615788b 100644 --- a/app/src/main/java/com/hormonetrack/ui/components/CurveChart.kt +++ b/app/src/main/java/com/hormonetrack/ui/components/CurveChart.kt @@ -168,17 +168,18 @@ fun CurveChart( } } - // Pics et creux (v1.2.3) : triangles aux extrema locaux, couleur de la série + // Pics et creux (v1.2.3) : triangles aux extrema locaux + valeur estimée + // (v1.2.6 : le triangle est accompagné de la valeur simulée du pic/creux) if (options.showExtrema) { PharmacokineticEngine.detectExtrema(s.points, { it.e2 }, minSwingAbs = 2.0) .forEach { ex -> - drawTriangle(xOf(ex.timestamp), yE2(ex.value), ex.isPeak, s.e2Style.color) + drawExtremum(xOf(ex.timestamp), yE2(ex.value), ex, s.e2Style.color) } if (options.showT) { s.tStyle?.let { tStyle -> PharmacokineticEngine.detectExtrema(s.points, { it.t }, minSwingAbs = 0.02) .forEach { ex -> - drawTriangle(xOf(ex.timestamp), yT(ex.value), ex.isPeak, tStyle.color) + drawExtremum(xOf(ex.timestamp), yT(ex.value), ex, tStyle.color) } } } @@ -219,24 +220,47 @@ fun CurveChart( } } -/** Triangle ▲ (pic, au-dessus du point) ou ▼ (creux, en dessous), couleur de la série. */ -private fun DrawScope.drawTriangle(cx: Float, cy: Float, isPeak: Boolean, color: Color) { +/** + * Triangle ▲ (pic, au-dessus du point) ou ▼ (creux, en dessous) + la VALEUR + * 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) { val r = 5.dp.toPx() val offset = 6.dp.toPx() val path = Path() - if (isPeak) { + val valueY: Float + if (extremum.isPeak) { val y = cy - offset path.moveTo(cx, y - r) path.lineTo(cx - r, y + r * 0.5f) path.lineTo(cx + r, y + r * 0.5f) + valueY = y - r - 3.dp.toPx() } else { val y = cy + offset path.moveTo(cx, y + r) path.lineTo(cx - r, y - r * 0.5f) path.lineTo(cx + r, y - r * 0.5f) + valueY = y + r + 11.dp.toPx() } path.close() drawPath(path, color) + // Valeur estimée du pic/creux, dans la couleur de la courbe + val paint = android.graphics.Paint().apply { + this.color = android.graphics.Color.argb( + (color.alpha * 255).toInt(), + (color.red * 255).toInt(), + (color.green * 255).toInt(), + (color.blue * 255).toInt() + ) + textSize = 9.dp.toPx() + textAlign = android.graphics.Paint.Align.CENTER + isAntiAlias = true + isFakeBoldText = true + } + drawContext.canvas.nativeCanvas.drawText( + formatValue(extremum.value), cx, valueY, paint + ) } private fun DrawScope.drawGrid(color: Color, padLeft: Float, padTop: Float, w: Float, h: Float, rows: Int) { for (i in 0..rows) { diff --git a/app/src/main/java/com/hormonetrack/ui/screens/SettingsScreen.kt b/app/src/main/java/com/hormonetrack/ui/screens/SettingsScreen.kt index 0a407c5..1594431 100644 --- a/app/src/main/java/com/hormonetrack/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/com/hormonetrack/ui/screens/SettingsScreen.kt @@ -348,11 +348,18 @@ fun SettingsScreen(onBack: () -> Unit) { showImportConfirm = false val json = importJson ?: return@TextButton CoroutineScope(Dispatchers.IO).launch { + // Import en mode ÉCRASEMENT : les données actuelles sont + // effacées d'abord (BackupManager), puis le tConfig du + // backup restaure DataStore et les rappels sont reprogrammés val result = try { BackupManager.importJson(repo, json) } catch (e: Exception) { null } + result?.let { + container.settings.setTConfig(it.tConfig) + AlarmScheduler(context).rescheduleAll(repo.allTreatmentsOnce()) + } withContext(Dispatchers.Main) { message = if (result != null) { context.getString(R.string.import_ok, result.treatments, result.doseLogs, result.labResults) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index af39302..5516303 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -149,10 +149,10 @@ Sauvegarde exportée. L\'export a échoué. L\'import a échoué : fichier invalide. - Importé : %1$d traitements, %2$d doses, %3$d analyses. - Restaurer + Restauré : %1$d traitements, %2$d doses, %3$d analyses (les données actuelles ont été remplacées, rappels reprogrammés). + Effacer & restaurer Importer une sauvegarde - Le contenu de la sauvegarde sera AJOUTÉ aux données actuelles (aucune suppression). + Le backup REMPLACERA toutes les données actuelles : les traitements, doses et analyses existants sont effacés d\'abord. Exporte une sauvegarde avant si tu veux les garder. À propos Profils PK issus des modèles d\'injection Estrannaise (EstraNase) et Transfem Science, extraits de Estrogen.ods (tables horaires D, k1–k3). diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d017d63..a10d5ee 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -149,10 +149,10 @@ Backup exported. Export failed. Import failed: invalid file. - Imported: %1$d treatments, %2$d doses, %3$d labs. - Restore + Restored: %1$d treatments, %2$d doses, %3$d labs (current data was replaced, reminders rescheduled). + Erase & restore Import backup - The backup content will be ADDED to the current data (no deletion). + The backup will REPLACE all current data: existing treatments, doses and labs are erased first. Export a backup now if you want to keep them. About PK profiles from Estrannaise (EstraNase) and Transfem Science injection models, extracted from Estrogen.ods (D, k1–k3 hourly tables). diff --git a/docs/DEVELOPPEMENT.md b/docs/DEVELOPPEMENT.md index 37843e6..7faccf2 100644 --- a/docs/DEVELOPPEMENT.md +++ b/docs/DEVELOPPEMENT.md @@ -506,7 +506,10 @@ gauche (pg/mL), T droite (ng/mL). Échelle « nice » (`niceCeil` : 1/2/2.5/5/10 partagée entre toutes les séries. Grille 4 lignes ; labels Y gauche/droite ; X : pas 6 h/24 h/5 j selon plage (`SimpleDateFormat` HH'h' / dd/MM). Labs : cercles (E2) et carrés (T) orange + valeur, **T convertie en ng/mL** (`convertTToNgMl`) au rendu. -Ligne verticale « maintenant ». +Ligne verticale « maintenant ». **Pics/creux** (v1.2.3, valeurs v1.2.6) : +triangles ▲▼ via `PharmacokineticEngine.detectExtrema` **accompagnés de la +valeur estimée** du pic (au-dessus) ou du creux (en dessous), dans la couleur +de la série (`drawExtremum`). Pièges : - `DrawScope` implémente `Density` → `X.dp.toPx()` direct ; ne PAS écrire de helper custom @@ -751,6 +754,32 @@ JSON casse UNIQUEMENT en release. ⚠️ À chaque activation d'optimisation : tester sur téléphone l'export/import de backup + les graphiques (R8 ne se vérifie pas en tests JVM). +### Checklist de déploiement complète (de A à Z, pour une session sans contexte) + +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` — 62 au total, + 44 si `local-test-data/` est absent (les régressions réelles sont skippées). +3. **Docs** : section `## [X.Y.Z]` en tête de `docs/CHANGELOG.md` (le corps des + releases Gitea en sera extrait automatiquement par le script), + §14 si bug + corrigé, + §2 (historique) si notable. +4. **Commit** (message descriptif par couche) + **tag annoté** : + `git tag -a vX.Y.Z -m "résumé"`. +5. **Push** : `git push origin main --tags` + `git push farewell main --tags` + (farewell : le repo doit exister sur l'instance ; push-to-create désactivé). +6. **Construire les 2 APK au niveau du tag** : + `git checkout vX.Y.Z` → `./gradlew assembleRelease assembleDebug` → copier + `app-release.apk` → `/tmp/apks/HormoneTrack-vX.Y.Z-release.apk` et + `app-debug.apk` → `HormoneTrack-vX.Y.Z-debug.apk` → `git checkout main`. +7. **Publier les releases** (corps = section CHANGELOG + les 2 APK attachés) : + `python3 scripts/gitea-release.py cloudyfy vX.Y.Z ` puis + idem avec `` ; répéter avec l'instance `farewell` (token + `gitea.farewell.dev` requis dans le trousseau, absent à ce jour). +8. **Smoke-test R8 sur téléphone** (le release APK n'est pas vérifiable en + tests JVM) : installation par-dessus l'existant, graphiques, export ET + import d'un backup JSON, rappel. Le debug APK est le repli (même signature). +9. **Mettre à jour §2/§8/§19/§21 si besoin** puis pusher la doc. + Notes : - L'APK est une **build debug** signée avec la clé debug locale — installable en sideload, mises à jour entre versions OK (même signature) ; @@ -864,4 +893,4 @@ Sur le téléphone de test (à compléter par l'utilisatrice) : sur le graphique** et la calibration couvre toujours ses périodes --- -*Doc mise à jour le 5 sept. 2026 (v1.2.5) — build OK, 62/62 tests verts (44 sans les données locales), dépôt Gitea privé + releases APK, aucune donnée de santé dans le dépôt ni l'historique.* +*Doc mise à jour le 5 sept. 2026 (v1.2.6) — build OK, 62/62 tests verts (44 sans les données locales), dépôts Gitea (cloudyfy + farewell) avec releases APK, aucune donnée de santé dans le dépôt ni l'historique.*