From fa5d2df5a53bde3aea961fbf505c76281f3604bd Mon Sep 17 00:00:00 2001 From: Siphonight Date: Sun, 6 Sep 2026 10:16:49 +0200 Subject: [PATCH] =?UTF-8?q?v1.3.1=20:=20en-t=C3=AAte=20=C2=AB=20temps=20so?= =?UTF-8?q?us=20THS=20=C2=BB=20sur=20Doses,=20logs=20de=20diagnostic=20exp?= =?UTF-8?q?ortables,=20fix=20toggle=20agenda=20(callback=20async)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DosesScreen : carte « Sous THS depuis le … » (HrtDuration, mois = 30 j estimés) - AppLog : journal horodaté buffer circulaire 500 lignes persisté filesDir/debug-log.txt ; export SAF + effacement dans Paramètres ; alimenté par agenda/permissions/import/export/erreurs - FIX toggle agenda (bug remonté) : le résultat de permission arrive ASYNCHRONE — le test synchrone post-launch() était toujours faux → le callback active le switch + journalisation - AppLogTest (4) + HrtDurationTest (5) → 87 tests verts - versionCode 15, versionName 1.3.1 --- README.md | 4 +- .../java/com/hormonetrack/HormoneTrackApp.kt | 2 + .../hormonetrack/ui/screens/DosesScreen.kt | 32 +++++++++ .../hormonetrack/ui/screens/SettingsScreen.kt | 41 +++++++++++ .../ui/screens/TreatmentEditorScreen.kt | 37 ++++++---- .../main/java/com/hormonetrack/util/AppLog.kt | 68 +++++++++++++++++++ .../java/com/hormonetrack/util/HrtDuration.kt | 15 ++++ app/src/main/res/values-fr/strings.xml | 7 ++ app/src/main/res/values/strings.xml | 7 ++ .../java/com/hormonetrack/util/AppLogTest.kt | 37 ++++++++++ .../com/hormonetrack/util/HrtDurationTest.kt | 38 +++++++++++ docs/CHANGELOG.md | 22 +++++- docs/DEVELOPPEMENT.md | 23 ++++++- docs/GUIDE_INSTALLATION.md | 3 + 14 files changed, 318 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/com/hormonetrack/util/AppLog.kt create mode 100644 app/src/main/java/com/hormonetrack/util/HrtDuration.kt create mode 100644 app/src/test/java/com/hormonetrack/util/AppLogTest.kt create mode 100644 app/src/test/java/com/hormonetrack/util/HrtDurationTest.kt diff --git a/README.md b/README.md index a3bdfbb..62b1491 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.0 — build Android ✅, **78 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.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** ✅ - **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) @@ -56,6 +56,8 @@ sauvegarde JSON. **100 % local, aucun compte, aucun serveur.** les notifications remontent sur la Watch GT 3 (Gadgetbridge ou Huawei Health) - **Nouveautés à chaque mise à jour** : dialog de changelog automatique (fermé = ne réapparaît pas avant la prochaine version) +- **Temps sous THS** affiché en haut de la page Doses (depuis la 1re prise) +- **Logs de diagnostic** exportables (Paramètres) — utile pour le support - **Événements d'agenda** : les rappels de prises peuvent créer un événement récurrent (posologie) dans un calendrier « HormoneTrack » de ton téléphone - **Sauvegarde/Restauration JSON** complète (traitements + doses + analyses + réglages T) diff --git a/app/src/main/java/com/hormonetrack/HormoneTrackApp.kt b/app/src/main/java/com/hormonetrack/HormoneTrackApp.kt index 143cbe9..1f03ea5 100644 --- a/app/src/main/java/com/hormonetrack/HormoneTrackApp.kt +++ b/app/src/main/java/com/hormonetrack/HormoneTrackApp.kt @@ -6,6 +6,7 @@ import com.hormonetrack.data.AppDatabase import com.hormonetrack.data.repository.HormoneRepository import com.hormonetrack.pk.PKProfileStore import com.hormonetrack.reminder.createNotificationChannel +import com.hormonetrack.util.AppLog import com.hormonetrack.settings.AppSettings class HormoneTrackApp : Application() { @@ -18,6 +19,7 @@ class HormoneTrackApp : Application() { container = AppContainer(this) PKProfileStore.init(this) createNotificationChannel(this) + AppLog.init(this) } } diff --git a/app/src/main/java/com/hormonetrack/ui/screens/DosesScreen.kt b/app/src/main/java/com/hormonetrack/ui/screens/DosesScreen.kt index eb69b2c..fc47df3 100644 --- a/app/src/main/java/com/hormonetrack/ui/screens/DosesScreen.kt +++ b/app/src/main/java/com/hormonetrack/ui/screens/DosesScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.AlertDialog import androidx.compose.material3.AssistChip import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon @@ -43,6 +44,7 @@ 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 import com.hormonetrack.ui.components.formatDose import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -100,6 +102,36 @@ fun DosesScreen() { TopAppBar( title = { Text(stringResource(R.string.nav_doses)) } ) + + // En-tête « temps sous THS » : de la 1re prise enregistrée à maintenant + val earliest = remember(doseLogs) { doseLogs.minOfOrNull { it.timestamp } } + earliest?.let { first -> + Card( + Modifier.fillMaxWidth().padding(vertical = 8.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer) + ) { + Column(Modifier.padding(12.dp)) { + val dateStr = DateTimeFormatter.ofPattern("d MMMM yyyy") + .withZone(ZoneId.systemDefault()) + .format(Instant.ofEpochMilli(first)) + Text( + stringResource(R.string.hrt_since, dateStr), + style = MaterialTheme.typography.titleMedium + ) + val totalDays = HrtDuration.daysSince(first, System.currentTimeMillis()) + if (totalDays > 0) { + val (months, days) = HrtDuration.monthsAndDays(totalDays) + Text( + if (months > 0) stringResource(R.string.hrt_duration, months, days) + else stringResource(R.string.hrt_duration_days_only, days), + style = MaterialTheme.typography.bodyLarge + ) + } + } + } + } + if (doseLogs.isEmpty()) { Text( stringResource(R.string.no_data), 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 5243587..9aa7411 100644 --- a/app/src/main/java/com/hormonetrack/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/com/hormonetrack/ui/screens/SettingsScreen.kt @@ -35,6 +35,7 @@ import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -46,6 +47,7 @@ import androidx.appcompat.app.AppCompatDelegate import androidx.core.os.LocaleListCompat import com.hormonetrack.R import com.hormonetrack.data.backup.BackupManager +import com.hormonetrack.util.AppLog import com.hormonetrack.pk.PharmacokineticEngine import com.hormonetrack.pk.TConfig import com.hormonetrack.reminder.AlarmScheduler @@ -75,6 +77,21 @@ fun SettingsScreen(onBack: () -> Unit) { var tKText by remember(tConfig.k) { mutableStateOf(formatDoubles(tConfig.k)) } var showImportConfirm by remember { mutableStateOf(false) } + var logLineCount by remember { mutableIntStateOf(AppLog.count()) } + val logExportLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.CreateDocument("text/plain") + ) { uri -> + uri ?: return@rememberLauncherForActivityResult + CoroutineScope(Dispatchers.IO).launch { + try { + context.contentResolver.openOutputStream(uri, "wt")?.use { os -> + os.write(AppLog.all().toByteArray(Charsets.UTF_8)) + os.flush() + } + withContext(Dispatchers.Main) { logLineCount = AppLog.count() } + } catch (_: Exception) { } + } + } var importJson by remember { mutableStateOf(null) } var message by remember { mutableStateOf(null) } @@ -322,6 +339,30 @@ fun SettingsScreen(onBack: () -> Unit) { } Spacer(Modifier.height(12.dp)) + // --- Logs de diagnostic (v1.3.1) --- + Card(Modifier.fillMaxWidth()) { + Column(Modifier.padding(12.dp)) { + Text(stringResource(R.string.logs_section), style = MaterialTheme.typography.titleMedium) + Text( + stringResource(R.string.logs_count, logLineCount), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(Modifier.height(6.dp)) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + OutlinedButton(onClick = { + val date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmm")) + logExportLauncher.launch("hormonetrack-logs-$date.txt") + }) { Text(stringResource(R.string.logs_export)) } + TextButton(onClick = { + AppLog.clear() + logLineCount = AppLog.count() + }) { Text(stringResource(R.string.logs_clear)) } + } + } + } + Spacer(Modifier.height(12.dp)) + // --- About : version installée + lien releases Gitea --- Card(Modifier.fillMaxWidth()) { Column(Modifier.padding(12.dp)) { 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 4f57148..db78991 100644 --- a/app/src/main/java/com/hormonetrack/ui/screens/TreatmentEditorScreen.kt +++ b/app/src/main/java/com/hormonetrack/ui/screens/TreatmentEditorScreen.kt @@ -56,6 +56,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import com.hormonetrack.reminder.AlarmScheduler import com.hormonetrack.reminder.CalendarEvents +import com.hormonetrack.util.AppLog import com.hormonetrack.ui.LocalAppContainer import com.hormonetrack.ui.components.formatDose import kotlinx.coroutines.CoroutineScope @@ -97,7 +98,13 @@ fun TreatmentEditorScreen(treatmentId: Long, onDone: () -> Unit) { var calendarPermissionGranted by remember { mutableStateOf(false) } val calendarPermissionLauncher = rememberLauncherForActivityResult( ActivityResultContracts.RequestMultiplePermissions() - ) { grants -> calendarPermissionGranted = grants.values.all { it } } + ) { grants -> + // ⚠️ FIX v1.3.1 : le résultat arrive ASYNCHRONE — c'est ICI (et pas + // dans le onCheckedChange synchrone) que le switch doit s'activer. + calendarPermissionGranted = grants.values.all { it } + AppLog.log("agenda", "permission agenda : accordée=$calendarPermissionGranted") + if (calendarPermissionGranted) calendarEnabled = true + } var showDeleteConfirm by remember { mutableStateOf(false) } var showPresetMenu by remember { mutableStateOf(false) } var showTimePicker by remember { mutableStateOf(false) } @@ -427,17 +434,18 @@ fun TreatmentEditorScreen(treatmentId: Long, onDone: () -> Unit) { Switch( checked = calendarEnabled, onCheckedChange = { want -> - if (want && !calendarPermissionGranted) { - calendarPermissionLauncher.launch( - arrayOf( - android.Manifest.permission.WRITE_CALENDAR, - android.Manifest.permission.READ_CALENDAR + if (want) { + if (calendarPermissionGranted) { + calendarEnabled = true + } else { + // le callback activera le switch si accordée + calendarPermissionLauncher.launch( + arrayOf( + android.Manifest.permission.WRITE_CALENDAR, + android.Manifest.permission.READ_CALENDAR + ) ) - ) - // activé seulement si la permission est accordée (callback) - if (calendarPermissionGranted) calendarEnabled = true - } else if (want) { - calendarEnabled = true + } } else { calendarEnabled = false } @@ -511,8 +519,11 @@ fun TreatmentEditorScreen(treatmentId: Long, onDone: () -> Unit) { if (eventToStore != saved.calendarEventId) { repo.updateTreatment(saved.copy(calendarEventId = eventToStore)) } - } catch (_: SecurityException) { - // permission agenda révoquée entre-temps → ignorer + AppLog.log("agenda", + "traitement #${saved.id} : événement agenda " + + if (eventToStore != null) "créé/mis à jour (id=$eventToStore)" else "supprimé") + } catch (e: Exception) { + AppLog.log("agenda", "ERREUR événement agenda : ${e.message}") } withContext(Dispatchers.Main) { onDone() } } diff --git a/app/src/main/java/com/hormonetrack/util/AppLog.kt b/app/src/main/java/com/hormonetrack/util/AppLog.kt new file mode 100644 index 0000000..fc883e9 --- /dev/null +++ b/app/src/main/java/com/hormonetrack/util/AppLog.kt @@ -0,0 +1,68 @@ +package com.hormonetrack.util + +import java.io.File +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +/** + * Journal de diagnostic de l'app (v1.3.1) : ligne horodatée par événement + * notable (agenda, rappels, import/export, erreurs), persisté dans + * `filesDir/debug-log.txt`, buffer circulaire de MAX_LINES. + * + * Exportable depuis Paramètres (« Logs de diagnostic ») pour le debug à + * distance — un utilisateur peut joindre le fichier à un rapport de bug. + * + * Les parties PURES (formatLine, trimLines) sont testables en JVM sans + * Android ; l'objet lui-même ne fait que l'IO fichier mince. + */ +object AppLog { + + const val MAX_LINES = 500 + + private val buffer = ArrayDeque() + private var file: File? = null + private val lock = Any() + + /** À appeler une fois dans Application.onCreate. */ + fun init(context: android.content.Context) { + synchronized(lock) { + val f = File(context.filesDir, "debug-log.txt") + file = f + if (f.exists()) { + buffer.clear() + buffer.addAll(f.readLines().takeLast(MAX_LINES)) + } + } + log("app", "démarrage (v init) — ${buffer.size} lignes de logs existantes") + } + + 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) + file?.appendText(line + "\n") + } + + fun all(): String = synchronized(lock) { buffer.joinToString("\n") } + + fun count(): Int = synchronized(lock) { buffer.size } + + fun clear() = synchronized(lock) { + buffer.clear() + file?.writeText("") + log("app", "logs effacés") + } + + /** Ligne horodatée : « 2026-09-06 12:34:56 [tag] message » (locale). */ + fun formatLine(tsMs: Long, tag: String, message: String): String { + val fmt = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) + return "${fmt.format(Date(tsMs))} [$tag] $message" + } + + /** Buffer circulaire : garde les MAX dernières lignes. */ + fun trimLines(existing: List, max: Int): List = + if (existing.size <= max) existing else existing.takeLast(max) +} diff --git a/app/src/main/java/com/hormonetrack/util/HrtDuration.kt b/app/src/main/java/com/hormonetrack/util/HrtDuration.kt new file mode 100644 index 0000000..318ac14 --- /dev/null +++ b/app/src/main/java/com/hormonetrack/util/HrtDuration.kt @@ -0,0 +1,15 @@ +package com.hormonetrack.util + +/** Estimation du temps passé sous THS (v1.3.1). Mois = 30 jours (estimation). */ +object HrtDuration { + + const val DAY_MS = 86_400_000L + + /** Jours entiers écoulés depuis la première prise (0 si futur/invalide). */ + fun daysSince(earliestMs: Long, nowMs: Long): Long = + if (nowMs <= earliestMs) 0L else (nowMs - earliestMs) / DAY_MS + + /** Décomposition mois(30 j) / jours restants. */ + fun monthsAndDays(totalDays: Long): Pair = + Pair(totalDays / 30, totalDays % 30) +} diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index a29a261..7edc268 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -33,6 +33,13 @@ Pics / creux Zoom avant (fenêtre plus courte) Zoom arrière (fenêtre plus longue) + Sous THS depuis le %1$s + %1$d mois et %2$d jours (%3$d jours au total) + %1$d jours + Logs de diagnostic + %1$d lignes (rappels, agenda, import/export, erreurs) + Exporter + Effacer Nouveautés de la version %1$s Version installée : %1$s Dernières versions sur Gitea (télécharger les APK) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5b012bd..255c432 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -33,6 +33,13 @@ Peaks / troughs Zoom in (shorter window) Zoom out (longer window) + Under HRT since %1$s + %1$d months and %2$d days (%3$d days total) + %1$d days + Diagnostic logs + %1$d lines (reminders, calendar, import/export, errors) + Export + Clear What\'s new in %1$s Installed version: %1$s Latest releases on Gitea (download APKs) diff --git a/app/src/test/java/com/hormonetrack/util/AppLogTest.kt b/app/src/test/java/com/hormonetrack/util/AppLogTest.kt new file mode 100644 index 0000000..a6a7603 --- /dev/null +++ b/app/src/test/java/com/hormonetrack/util/AppLogTest.kt @@ -0,0 +1,37 @@ +package com.hormonetrack.util + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +/** Formatage et buffer circulaire des logs de diagnostic (v1.3.1). */ +class AppLogTest { + + @Test + fun `ligne horodatée au format attendu`() { + val line = AppLog.formatLine(1_788_748_800_000L, "agenda", "événement créé") + assertTrue(line.contains("[agenda]")) + assertTrue(line.contains("événement créé")) + assertTrue(Regex("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2} \\[agenda\\] événement créé$").matches(line)) + } + + @Test + fun `trim garde les MAX dernières lignes`() { + val existing = (1..600).map { "ligne $it" } + val trimmed = AppLog.trimLines(existing, 500) + assertEquals(500, trimmed.size) + assertEquals("ligne 101", trimmed.first()) + assertEquals("ligne 600", trimmed.last()) + } + + @Test + fun `trim ne fait rien si sous la limite`() { + val existing = (1..100).map { "ligne $it" } + assertEquals(100, AppLog.trimLines(existing, 500).size) + } + + @Test + fun `trim liste vide`() { + assertEquals(0, AppLog.trimLines(emptyList(), 500).size) + } +} diff --git a/app/src/test/java/com/hormonetrack/util/HrtDurationTest.kt b/app/src/test/java/com/hormonetrack/util/HrtDurationTest.kt new file mode 100644 index 0000000..fab6d6d --- /dev/null +++ b/app/src/test/java/com/hormonetrack/util/HrtDurationTest.kt @@ -0,0 +1,38 @@ +package com.hormonetrack.util + +import org.junit.Assert.assertEquals +import org.junit.Test + +/** Estimation du temps sous THS : 1re prise → maintenant, mois = 30 j. */ +class HrtDurationTest { + + private val day = HrtDuration.DAY_MS + + @Test + fun `même jour - zéro jour`() { + assertEquals(0L, HrtDuration.daysSince(1000L, 1000L)) + assertEquals(0L, HrtDuration.daysSince(2000L, 1000L)) + } + + @Test + fun `un jour exact`() { + assertEquals(1L, HrtDuration.daysSince(1000L, 1000L + day)) + } + + @Test + fun `365 jours - un an`() { + assertEquals(365L, HrtDuration.daysSince(0L, 365 * day)) + } + + @Test + fun `décomposition mois et jours - 400 jours = 13 mois et 10 jours`() { + val (months, days) = HrtDuration.monthsAndDays(400L) + assertEquals(13L, months) + assertEquals(10L, days) + } + + @Test + fun `décomposition - zéro jour`() { + assertEquals(Pair(0L, 0L), HrtDuration.monthsAndDays(0L)) + } +} diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 9752c1b..9d20201 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,7 +1,27 @@ # 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.0`, …). +Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.3.1`, …). + +## [1.3.1] — 2026-09-06 (versionCode 15) + +### Ajouté +- **En-tête « temps sous THS »** sur la page Doses : depuis la 1re prise + enregistrée, en jours totaux et décomposition mois/jours (mois = 30 j, + estimation documentée). +- **Logs de diagnostic** (v1.3.1) : journal horodaté des événements notables + (agenda, rappels, import/export, erreurs), buffer circulaire de 500 lignes, + persisté dans `filesDir/debug-log.txt` ; export (SAF) et effacement depuis + Paramètres. Objectif : debug à distance via les logs joints aux retours. +- 9 nouveaux tests (87 au total) : AppLog (formatage, buffer circulaire) et + HrtDuration (jours, décomposition mois/jours). + +### Corrigé +- **Toggle de l'agenda ne s'activait pas** (bug remonté) : le résultat de la + permission arrive ASYNCHRONE — le test synchrone juste après `launch()` + était toujours faux → le switch rebondissait sans s'activer et sans + feedback. FIX : le callback active le switch ; feedback visible (hint) et + journalisation (AppLog). ## [1.3.0] — 2026-09-06 (versionCode 14) diff --git a/docs/DEVELOPPEMENT.md b/docs/DEVELOPPEMENT.md index d40aea6..349d823 100644 --- a/docs/DEVELOPPEMENT.md +++ b/docs/DEVELOPPEMENT.md @@ -76,6 +76,7 @@ Fonctionnalités v1 : | 5 sept. 2026 (session v1.2.1) | Remontées UX/utilisateur : calibration **par période d'ester** (labs EV → doses EV, labs EEn → doses EEn — corrige les courbes gonflées), lab T "pg/mL" neutralisé, **édition des labs**, tap accueil → Graphiques + mini-légende, auto-cal fusionnée (E2+T, un seul switch) ; 2ᵉ régression épinglée sur le nouvel export ; **44 tests verts**, APK v1.2.1. | | 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 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. | @@ -348,7 +349,7 @@ Type de retour : `LevelPoint(timestamp, e2, t)`. ## 8. Tests unitaires -**78 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — 44 sans les +**87 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — 44 sans les données de test locales (cf §8.bis). Dépendance : JUnit 4.13.2. Emplacement : `app/src/test/java/com/hormonetrack/`. Répertoire de travail d'exécution = `app/` → l'asset est lu via `src/main/assets/pk_profiles.json` (fallback `app/src/…`). @@ -424,6 +425,10 @@ dépôt (ni maintenant, ni si le repo devient public un jour). (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`** (5) : 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`) : @@ -493,7 +498,9 @@ Sur la montre : remontée par Gadgetbridge **ou** Huawei Health (cf §17). 0,02 ng/mL) - `DosesScreen` : LazyColumn par jour (desc), **Δ jours depuis la dose précédente du même traitement** (`intervalsByDoseId`, colonne « Interval (d) » du `.ods`), suppression - avec confirmation, FAB → `DoseDialog` (création), **tap sur la ligne → édition** + avec confirmation, FAB → `DoseDialog` (création), **tap sur la ligne → édition** ; + **en-tête « temps sous THS »** (v1.3.1) : carte depuis la 1re prise, jours + totaux + décomposition mois/jours (`HrtDuration`, mois = 30 j estimés) - `LabsScreen` (v1.2.2) : **prise de sang E2 + T en une entrée** (LabDialog create : deux sections optionnelles → 1 ou 2 LabResult au même timestamp) ; affichage **groupé par timestamp** (`groupLabsForDisplay` : E2 avant T, autres ensuite, tri @@ -509,6 +516,11 @@ Sur la montre : remontée par Gadgetbridge **ou** Huawei Health (cf §17). switch « Simuler les doses à venir » + intervalle en jours)** ; carte Rappel (switch + TimePicker **centré** + avertissement alarmes exactes) ; switch actif ; save → insert/update + schedule/cancel ; delete avec confirmation ; `createdAt` préservé +- **Logs de diagnostic (v1.3.1, `util/AppLog.kt`)** : journal horodaté + (buffer circulaire 500 lignes, persisté `filesDir/debug-log.txt`), + alimenté par l'agenda (permissions, upsert/delete), l'import/export et les + erreurs ; **export (SAF) et effacement** dans Paramètres — permet le debug + à distance (l'utilisateur joint les logs à son retour). - **Dialog « Nouveautés » (v1.3.0)** : au démarrage, si `BuildConfig.VERSION_NAME` est plus récente que la dernière vue (DataStore `changelog_seen_version`), un AlertDialog affiche les sections CHANGELOG concernées (extraites par @@ -694,6 +706,11 @@ 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. +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 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. → @@ -1007,4 +1024,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.0) — 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.* +*Doc mise à jour le 6 sept. 2026 (v1.3.1) — 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.* diff --git a/docs/GUIDE_INSTALLATION.md b/docs/GUIDE_INSTALLATION.md index eedb9ea..c94af2c 100644 --- a/docs/GUIDE_INSTALLATION.md +++ b/docs/GUIDE_INSTALLATION.md @@ -86,6 +86,9 @@ Les notifications de l'app remontent automatiquement sur la montre via **Huawei passe-le **inactif** (switch « Actif ») : il disparaît de la saisie et de tes rappels, mais **son historique reste simulé et calibré** sur les graphiques — parfait pour une transition valerate → enanthate + - **Agenda** : active « Événement d'agenda récurrent » → un événement se + répétant tous les N jours (Posologie) apparaît dans le calendrier + « HormoneTrack » de ton téléphone - Ester (EV / EU / EEn) + modèle (Estrannaise / Transfem Science) = les courbes du `.ods` - Pour gel/patch/oral : paramètres Bateman (temps au pic, demi-vie, biodispo) 2. **Doses → +** → logue tes injections passées