v1.3.1 : en-tête « temps sous THS » sur Doses, logs de diagnostic exportables, fix toggle agenda (callback async)

- 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
This commit is contained in:
Siphonight 2026-09-06 10:16:49 +02:00
parent d456cfa093
commit fa5d2df5a5
14 changed files with 318 additions and 18 deletions

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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),

View File

@ -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<String?>(null) }
var message by remember { mutableStateOf<String?>(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)) {

View File

@ -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() }
}

View File

@ -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<String>()
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<String>, max: Int): List<String> =
if (existing.size <= max) existing else existing.takeLast(max)
}

View File

@ -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<Long, Long> =
Pair(totalDays / 30, totalDays % 30)
}

View File

@ -33,6 +33,13 @@
<string name="show_extrema">Pics / creux</string>
<string name="zoom_in">Zoom avant (fenêtre plus courte)</string>
<string name="zoom_out">Zoom arrière (fenêtre plus longue)</string>
<string name="hrt_since">Sous THS depuis le %1$s</string>
<string name="hrt_duration">%1$d mois et %2$d jours (%3$d jours au total)</string>
<string name="hrt_duration_days_only">%1$d jours</string>
<string name="logs_section">Logs de diagnostic</string>
<string name="logs_count">%1$d lignes (rappels, agenda, import/export, erreurs)</string>
<string name="logs_export">Exporter</string>
<string name="logs_clear">Effacer</string>
<string name="changelog_title">Nouveautés de la version %1$s</string>
<string name="settings_version">Version installée : %1$s</string>
<string name="releases_link">Dernières versions sur Gitea (télécharger les APK)</string>

View File

@ -33,6 +33,13 @@
<string name="show_extrema">Peaks / troughs</string>
<string name="zoom_in">Zoom in (shorter window)</string>
<string name="zoom_out">Zoom out (longer window)</string>
<string name="hrt_since">Under HRT since %1$s</string>
<string name="hrt_duration">%1$d months and %2$d days (%3$d days total)</string>
<string name="hrt_duration_days_only">%1$d days</string>
<string name="logs_section">Diagnostic logs</string>
<string name="logs_count">%1$d lines (reminders, calendar, import/export, errors)</string>
<string name="logs_export">Export</string>
<string name="logs_clear">Clear</string>
<string name="changelog_title">What\'s new in %1$s</string>
<string name="settings_version">Installed version: %1$s</string>
<string name="releases_link">Latest releases on Gitea (download APKs)</string>

View File

@ -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)
}
}

View File

@ -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))
}
}

View File

@ -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)

View File

@ -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.*

View File

@ -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