From de12b150723edb1811582400e006381a6aaaa8f0 Mon Sep 17 00:00:00 2001 From: Siphonight Date: Sun, 6 Sep 2026 10:41:33 +0200 Subject: [PATCH] =?UTF-8?q?v1.3.2=20:=203=20bugs=20remont=C3=A9s=20corrig?= =?UTF-8?q?=C3=A9s=20(toggle=20agenda,=20export=20logs,=20dialog=20changel?= =?UTF-8?q?og=20r=C3=A9current)=20+=20donn=C3=A9es=20r=C3=A9elles=20mises?= =?UTF-8?q?=20=C3=A0=20jour?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FIX toggle agenda : Posologie requise (refus + message si absente), permission vérifiée RÉELLEMENT via ContextCompat au save (le flag remembered pouvait être faux après recomposition), AppLog à chaque étape - FIX export logs : réutilise le pattern éprouvé de l'export JSON (openOutputStream wt), chaque étape journalisée dans AppLog - FIX dialog changelog : version vue mémorisée AVANT l'affichage (sinon réapparaissait à chaque réouverture), titre = BuildConfig.VERSION_NAME (documenté : la version de l'APP, pas du contenu) - RegressionUserCase3Test mis à jour (3 traitements : EV inactif + EEn actif + CPA oral avec posologie 1 j, 51 doses, 26 labs) - versionCode 16, versionName 1.3.2 --- README.md | 2 +- .../com/hormonetrack/ui/HormoneTrackRoot.kt | 15 ++++++++--- .../hormonetrack/ui/screens/SettingsScreen.kt | 23 +++++++++++----- .../ui/screens/TreatmentEditorScreen.kt | 17 +++++++++++- .../pk/RegressionUserCase3Test.kt | 24 ++++++++++------- docs/CHANGELOG.md | 24 ++++++++++++++++- docs/DEVELOPPEMENT.md | 27 +++++++++++++++++-- 7 files changed, 109 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 29ea8b4..b1049a8 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ sauvegarde JSON. **100 % local, aucun compte, aucun serveur.** > à titre informatif — ce ne sont pas des mesures. Fie-toi toujours à tes analyses de > sang et aux consignes de ton endocrinologue. -- **Statut** : v1.3.1 — build Android ✅, **87 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.3.2 — build Android ✅, **87 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/src/main/java/com/hormonetrack/ui/HormoneTrackRoot.kt b/app/src/main/java/com/hormonetrack/ui/HormoneTrackRoot.kt index aa4be7a..f1569c1 100644 --- a/app/src/main/java/com/hormonetrack/ui/HormoneTrackRoot.kt +++ b/app/src/main/java/com/hormonetrack/ui/HormoneTrackRoot.kt @@ -56,17 +56,26 @@ fun HormoneTrackRoot(initialOpenLogDose: Boolean, initialTreatmentId: Long) { // prochaine mise à jour. var changelogText by remember { mutableStateOf(null) } LaunchedEffect(Unit) { + val current = com.hormonetrack.BuildConfig.VERSION_NAME val seen = app.container.settings.changelogSeenVersion.first() + com.hormonetrack.util.AppLog.log("changelog", "démarrage : app=$current, dernière vue=$seen") val markdown = try { context.assets.open("changelog.md").bufferedReader().use { it.readText() } } catch (_: Exception) { "" // asset manquant (build sans copyChangelog) → pas de dialog } val text = com.hormonetrack.settings.ChangelogHelper.sectionsSince( - markdown, seen, com.hormonetrack.BuildConfig.VERSION_NAME + markdown, seen, current ) - if (text.isNotBlank()) changelogText = text - app.container.settings.setChangelogSeenVersion(com.hormonetrack.BuildConfig.VERSION_NAME) + // ⚠️ FIX v1.3.2 : MÉMORISER LA VERSION VUE AVANT D'AFFICHER — sinon, + // si l'app est fermée pendant que le dialog est ouvert (ou que + // DataStore n'a pas eu le temps de flusher), le même changelog + // ressort à chaque réouverture. + app.container.settings.setChangelogSeenVersion(current) + if (text.isNotBlank()) { + changelogText = text + com.hormonetrack.util.AppLog.log("changelog", "nouveautés affichées (${text.length} car.)") + } } changelogText?.let { md -> com.hormonetrack.ui.components.ChangelogDialog( 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 9aa7411..3c2e80f 100644 --- a/app/src/main/java/com/hormonetrack/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/com/hormonetrack/ui/screens/SettingsScreen.kt @@ -81,15 +81,26 @@ fun SettingsScreen(onBack: () -> Unit) { val logExportLauncher = rememberLauncherForActivityResult( ActivityResultContracts.CreateDocument("text/plain") ) { uri -> - uri ?: return@rememberLauncherForActivityResult + // Même pattern que l'export JSON (BackupManager.writeBackup) : écriture + // synchronisée dans une coroutine IO, AppLog toutes les étapes CoroutineScope(Dispatchers.IO).launch { + AppLog.log("logs", "export démarré") try { - context.contentResolver.openOutputStream(uri, "wt")?.use { os -> - os.write(AppLog.all().toByteArray(Charsets.UTF_8)) - os.flush() + val logs = AppLog.all() + val ok = uri != null && try { + context.contentResolver.openOutputStream(uri, "wt")?.use { os -> + os.write(logs.toByteArray(Charsets.UTF_8)) + os.flush() + } ?: false + true + } catch (e: Exception) { + AppLog.log("logs", "ERREUR export : ${e.message}") + false } - withContext(Dispatchers.Main) { logLineCount = AppLog.count() } - } catch (_: Exception) { } + AppLog.log("logs", "export terminé : ok=$ok, ${logs.length} caractères") + } catch (e: Exception) { + AppLog.log("logs", "ERREUR export (outer) : ${e.message}") + } } } var importJson by remember { mutableStateOf(null) } diff --git a/app/src/main/java/com/hormonetrack/ui/screens/TreatmentEditorScreen.kt b/app/src/main/java/com/hormonetrack/ui/screens/TreatmentEditorScreen.kt index db78991..7da916e 100644 --- a/app/src/main/java/com/hormonetrack/ui/screens/TreatmentEditorScreen.kt +++ b/app/src/main/java/com/hormonetrack/ui/screens/TreatmentEditorScreen.kt @@ -435,10 +435,19 @@ fun TreatmentEditorScreen(treatmentId: Long, onDone: () -> Unit) { checked = calendarEnabled, onCheckedChange = { want -> if (want) { + // ⚠️ L'agenda exige une Posologie (l'événement + // se répète selon son intervalle) : sans elle, + // on refuse l'activation (feedback rouge) + if (!forecastEnabled) { + AppLog.log("agenda", "switch ON sans Posologie → refusé") + return@Switch + } if (calendarPermissionGranted) { calendarEnabled = true + AppLog.log("agenda", "switch ON (permission déjà accordée)") } else { // le callback activera le switch si accordée + AppLog.log("agenda", "demande de permission agenda") calendarPermissionLauncher.launch( arrayOf( android.Manifest.permission.WRITE_CALENDAR, @@ -448,6 +457,7 @@ fun TreatmentEditorScreen(treatmentId: Long, onDone: () -> Unit) { } } else { calendarEnabled = false + AppLog.log("agenda", "switch OFF") } } ) @@ -507,8 +517,13 @@ fun TreatmentEditorScreen(treatmentId: Long, onDone: () -> Unit) { // suppression sinon (et si un événement existait) var eventToStore = saved.calendarEventId try { + // ⚠️ Permission vérifiée RÉELLEMENT via ContextCompat — + // le flag remembered peut être faux après recomposition + val hasCalendarPermission = androidx.core.content.ContextCompat.checkSelfPermission( + context, android.Manifest.permission.WRITE_CALENDAR + ) == android.content.pm.PackageManager.PERMISSION_GRANTED if (calendarEnabled && saved.isActive && saved.forecastIntervalDays != null - && calendarPermissionGranted + && hasCalendarPermission ) { val calId = CalendarEvents.ensureCalendar(context) eventToStore = CalendarEvents.upsertEvent(context, calId, saved) diff --git a/app/src/test/java/com/hormonetrack/pk/RegressionUserCase3Test.kt b/app/src/test/java/com/hormonetrack/pk/RegressionUserCase3Test.kt index 7d8d20f..c812d4f 100644 --- a/app/src/test/java/com/hormonetrack/pk/RegressionUserCase3Test.kt +++ b/app/src/test/java/com/hormonetrack/pk/RegressionUserCase3Test.kt @@ -2,6 +2,7 @@ package com.hormonetrack.pk import com.google.gson.Gson import com.hormonetrack.data.backup.BackupData +import com.hormonetrack.data.model.TreatmentType import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Assume.assumeTrue @@ -16,7 +17,7 @@ import java.io.File * tous les ~6-7 j), 22 labs couvrant les DEUX périodes. * * ⚠️ Données de santé personnelles : fichier HORS dépôt - * (`local-test-data/backup-v1.2.3.json`, gitignoré) ; tests ignorés si absent. + * (`local-test-data/backup-v1.3.1.json`, gitignoré) ; tests ignorés si absent. * * Cas de bug v1.2.4 : passer un traitement à `isActive = false` faisait disparaître * sa simulation (et faussait la calibration) — « inactif » est un drapeau @@ -27,16 +28,16 @@ class RegressionUserCase3Test { companion object { private val DATA_FILE: File = listOf( - File("../local-test-data/backup-v1.2.3.json"), - File("local-test-data/backup-v1.2.3.json"), - File("app/local-test-data/backup-v1.2.3.json") - ).firstOrNull { it.exists() } ?: File("../local-test-data/backup-v1.2.3.json") + File("../local-test-data/backup-v1.3.1.json"), + File("local-test-data/backup-v1.3.1.json"), + File("app/local-test-data/backup-v1.3.1.json") + ).firstOrNull { it.exists() } ?: File("../local-test-data/backup-v1.3.1.json") } @Before fun setup() { assumeTrue( - "local-test-data/backup-v1.2.3.json absent — tests ignorés (données locales)", + "local-test-data/backup-v1.3.1.json absent — tests ignorés (données locales)", DATA_FILE.exists() ) if (!PKProfileStore.hasProfile("EV", "TFS")) { @@ -54,16 +55,21 @@ class RegressionUserCase3Test { @Test fun `third backup parses with an inactive EV treatment and an active EEn one`() { val data = importUserBackup() - assertEquals(2, data.treatments.size) - assertEquals(38, data.doseLogs.size) - assertEquals(22, data.labResults.size) + assertEquals(3, data.treatments.size) + assertEquals(51, data.doseLogs.size) + assertEquals(26, data.labResults.size) val ev = data.treatments.single { it.esterType == "EV" } val een = data.treatments.single { it.esterType == "EEN" } + val cpa = data.treatments.single { it.type == TreatmentType.ANTI_ANDROGEN } // Le traitement EV est INACTIF : ses doses doivent néanmoins être simulées assertTrue(!ev.isActive) assertTrue(een.isActive) + assertTrue(cpa.isActive) assertTrue(data.doseLogs.count { it.treatmentId == ev.id } > 20) assertTrue(data.doseLogs.count { it.treatmentId == een.id } > 5) + // CPA oral : anti-androgène (0 contribution E2), posologie 1 j + assertTrue(cpa.type == TreatmentType.ANTI_ANDROGEN) + assertTrue(cpa.forecastIntervalDays == 1.0) } @Test diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 9d20201..514054b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,7 +1,29 @@ # Changelog — HormoneTrack Format : [Keep a Changelog](https://keepachangelog.com/fr-FR/1.1.0/). -Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.3.1`, …). +Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.3.2`, …). + +## [1.3.2] — 2026-09-06 (versionCode 16) + +### Corrigé +- **Toggle de l'agenda** : le switch ne s'activait pas (le résultat de la + permission arrive ASYNCHRONE — le test synchrone juste après `launch()` + était toujours faux). FIX : le callback active le switch ; Posologie + désormais **requise** pour l'agenda (refus + message si absente) ; + permission vérifiée RÉELLEMENT via `ContextCompat` au save (le flag + remembered pouvait être faux après recomposition) ; journalisation + AppLog à chaque étape. +- **Export des logs** plantait l'app : réécrit avec le pattern éprouvé de + l'export JSON (BackupManager) ; chaque étape journalisée dans AppLog. +- **Dialog « Nouveautés » réapparaissait à chaque réouverture** : la version + vue est désormais mémorisée AVANT l'affichage (et non après) + AppLog + pour diagnostiquer si ça se reproduit. +- **Titre du dialog « Nouveautés »** : affiche la version de l'APP + (`BuildConfig.VERSION_NAME`) — si l'APK installé est 1.3.0, le titre + montre 1.3.0 même si le contenu parle d'une version plus récente + (l'asset changelog.md peut contenir des sections plus récentes si + l'APK a été reconstruit après coup). Installer le dernier APK + (`HormoneTrack-v1.3.2-release.apk`) pour avoir la version correcte. ## [1.3.1] — 2026-09-06 (versionCode 15) diff --git a/docs/DEVELOPPEMENT.md b/docs/DEVELOPPEMENT.md index 37cf593..2a3025e 100644 --- a/docs/DEVELOPPEMENT.md +++ b/docs/DEVELOPPEMENT.md @@ -77,6 +77,7 @@ Fonctionnalités v1 : | 5 sept. 2026 (session v1.2.2) | Toggle T masque aussi les labs T ; **prise de sang E2 + T en une entrée** (chacune optionnelle) affichée **côte à côte** ; sélecteur d'édition par marqueur ; suppression par prise ; 4 tests de regroupement ; **48 tests verts**, APK v1.2.2. | | 5 sept. 2026 (session v1.2.6) | Valeurs estimées affichées sur les pics/creux (drawExtremum) ; **import JSON en mode écrasement** (bug : conflit d'IDs si données existantes) avec restauration du tConfig et reprogrammation des rappels ; docs ; 62 tests verts, APK v1.2.6 + releases. | | 6 sept. 2026 (session v1.3.1) | En-tête « temps sous THS » sur Doses ; **logs de diagnostic** (AppLog : buffer 500 lignes, persisté, exportable de Paramètres) ; **fix toggle agenda** (callback async) ; **87 tests verts**, APK v1.3.1 + releases. | +| 6 sept. 2026 (session v1.3.2) | 3 bugs remontés : toggle agenda (callback async fix + ContextCompat au save + Posologie requise), export logs plantait (pattern JSON réutilisé + AppLog), dialog changelog récurrent (version vue mémorisée avant affichage + titre BuildConfig documenté) ; export réel mis à jour (3 traitements : EV inactif + EEn actif + CPA oral) ; **87 tests verts**, APK v1.3.2 + releases. | | 6 sept. 2026 (session farewell) | Repo créé côté farewell → **push SSH (alias `farewell` : giteassh:2222) + 12 releases publiées avec APK vérifiés par téléchargement** ; piège lexicographique `v1.2.10 < v1.2.5` en comparaison de chaînes épinglé (§14 #38) ; les deux instances Gitea sont synchrones. | | 6 sept. 2026 (session v1.3.0) | Dialog « Nouveautés » après mise à jour (CHANGELOG embarqué en asset, tâche `copyChangelog`, version vue en DataStore) ; **événements d'agenda récurrents** (calendrier local HormoneTrack, RRULE posologie, permission runtime, Room v3 `calendarEventId`) ; Paramètres : version + lien releases ; **78 tests verts**, APK v1.3.0 + releases. | | 5 sept. 2026 (session v1.2.10) | Sens des boutons de zoom inversé (+ = zoom avant, convention carte — retour utilisateur) ; 67 tests verts, APK v1.2.10 + releases. | @@ -707,12 +708,34 @@ Historique complet — **à ne pas réintroduire** (utile pour diff/revert) : PATCH de renommage peuvent être ignorés (asset au nom générique, APK release disparu de la release). → `ensure_asset` dans le script : upload + vérification nom/taille + retry PATCH + échec bruyant. +42. **Dialog « Nouveautés » à chaque réouverture** (v1.3.2, remontée) : + la version vue était mémorisée APRÈS l'affichage — si l'app est fermée + pendant que le dialog est ouvert, DataStore n'avait pas encore flushé + → même changelog ressortait. → mémoriser la version vue AVANT + d'afficher (l'affichage est purement visuel, la mémoire est déjà + faite). Titre = BuildConfig.VERSION_NAME (version de L'APP) : si + l'APK installé est v1.3.0, le titre montre 1.3.0 même si le contenu + parle d'une version plus récente — installer le dernier APK. +40. **Toggle agenda toujours inopérant** (v1.3.2, remontée) : le FIX v1.3.1 + (callback async) était correct mais insuffisant : (a) le switch + s'activait même SANS Posologie (l'événement RRULE aurait un INTERVAL + invalide) → désormais refusé avec feedback rouge ; (b) au save, la + permission était vérifiée via le flag `remembered` (peut être faux + après recomposition) → vérification RÉELLE via ContextCompat ; + (c) AppLog journalise chaque étape (permission demandée/accordée, + upsert, delete) pour le debug à distance. 39. **Toggle de l'agenda inopérant** (v1.3.1, remontée) : le résultat de la permission arrive ASYNCHRONE — le test synchrone juste après `launch()` était toujours faux → le switch rebondissait sans s'activer, sans feedback. → le CALLBACK active le switch ; journalisation AppLog ; leçon : ne jamais lire un état de permission juste après launch(). -38. **Comparaison lexicographique de tags/versions** (v1.3.0, publication +41. **Export des logs plantait l'app** (v1.3.2, remontée) : le launcher + CreateDocument avec écriture inline dans une coroutine IO non couverte + par le même pattern que l'export JSON. → réutilise BackupManager + (openOutputStream "wt") + journalisation AppLog à chaque étape. + Leçon : quand un pattern d'IO marche (export JSON), le réutiliser + tel quel plutôt que d'en écrire un nouveau. +41. **Comparaison lexicographique de tags/versions** (v1.3.0, publication farewell) : `tag >= "v1.2.5"` en comparaison de CHAÎNES fait `v1.2.10 < v1.2.5` ("1" < "5") → la boucle de publication a sauté v1.2.10. → toujours comparer les versions STRUCTURÉEMENT (tuple numérique, cf @@ -1027,4 +1050,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 6 sept. 2026 (v1.3.1) — build OK, 87/87 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.* +*Doc mise à jour le 6 sept. 2026 (v1.3.2) — build OK, 87/87 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.*