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:
parent
d456cfa093
commit
fa5d2df5a5
@ -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
|
> à titre informatif — ce ne sont pas des mesures. Fie-toi toujours à tes analyses de
|
||||||
> sang et aux consignes de ton endocrinologue.
|
> 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)
|
- **Journal des versions** : [docs/CHANGELOG.md](docs/CHANGELOG.md)
|
||||||
- **Guide utilisateur** : [docs/GUIDE_INSTALLATION.md](docs/GUIDE_INSTALLATION.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)
|
- **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)
|
les notifications remontent sur la Watch GT 3 (Gadgetbridge ou Huawei Health)
|
||||||
- **Nouveautés à chaque mise à jour** : dialog de changelog automatique
|
- **Nouveautés à chaque mise à jour** : dialog de changelog automatique
|
||||||
(fermé = ne réapparaît pas avant la prochaine version)
|
(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
|
- **É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
|
récurrent (posologie) dans un calendrier « HormoneTrack » de ton téléphone
|
||||||
- **Sauvegarde/Restauration JSON** complète (traitements + doses + analyses + réglages T)
|
- **Sauvegarde/Restauration JSON** complète (traitements + doses + analyses + réglages T)
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import com.hormonetrack.data.AppDatabase
|
|||||||
import com.hormonetrack.data.repository.HormoneRepository
|
import com.hormonetrack.data.repository.HormoneRepository
|
||||||
import com.hormonetrack.pk.PKProfileStore
|
import com.hormonetrack.pk.PKProfileStore
|
||||||
import com.hormonetrack.reminder.createNotificationChannel
|
import com.hormonetrack.reminder.createNotificationChannel
|
||||||
|
import com.hormonetrack.util.AppLog
|
||||||
import com.hormonetrack.settings.AppSettings
|
import com.hormonetrack.settings.AppSettings
|
||||||
|
|
||||||
class HormoneTrackApp : Application() {
|
class HormoneTrackApp : Application() {
|
||||||
@ -18,6 +19,7 @@ class HormoneTrackApp : Application() {
|
|||||||
container = AppContainer(this)
|
container = AppContainer(this)
|
||||||
PKProfileStore.init(this)
|
PKProfileStore.init(this)
|
||||||
createNotificationChannel(this)
|
createNotificationChannel(this)
|
||||||
|
AppLog.init(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import androidx.compose.material.icons.filled.Delete
|
|||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.AssistChip
|
import androidx.compose.material3.AssistChip
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@ -43,6 +44,7 @@ import com.hormonetrack.data.model.DoseLog
|
|||||||
import com.hormonetrack.data.model.Treatment
|
import com.hormonetrack.data.model.Treatment
|
||||||
import com.hormonetrack.ui.LocalAppContainer
|
import com.hormonetrack.ui.LocalAppContainer
|
||||||
import com.hormonetrack.ui.components.DoseDialog
|
import com.hormonetrack.ui.components.DoseDialog
|
||||||
|
import com.hormonetrack.util.HrtDuration
|
||||||
import com.hormonetrack.ui.components.formatDose
|
import com.hormonetrack.ui.components.formatDose
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -100,6 +102,36 @@ fun DosesScreen() {
|
|||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text(stringResource(R.string.nav_doses)) }
|
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()) {
|
if (doseLogs.isEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.no_data),
|
stringResource(R.string.no_data),
|
||||||
|
|||||||
@ -35,6 +35,7 @@ import androidx.compose.material3.TopAppBar
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@ -46,6 +47,7 @@ import androidx.appcompat.app.AppCompatDelegate
|
|||||||
import androidx.core.os.LocaleListCompat
|
import androidx.core.os.LocaleListCompat
|
||||||
import com.hormonetrack.R
|
import com.hormonetrack.R
|
||||||
import com.hormonetrack.data.backup.BackupManager
|
import com.hormonetrack.data.backup.BackupManager
|
||||||
|
import com.hormonetrack.util.AppLog
|
||||||
import com.hormonetrack.pk.PharmacokineticEngine
|
import com.hormonetrack.pk.PharmacokineticEngine
|
||||||
import com.hormonetrack.pk.TConfig
|
import com.hormonetrack.pk.TConfig
|
||||||
import com.hormonetrack.reminder.AlarmScheduler
|
import com.hormonetrack.reminder.AlarmScheduler
|
||||||
@ -75,6 +77,21 @@ fun SettingsScreen(onBack: () -> Unit) {
|
|||||||
var tKText by remember(tConfig.k) { mutableStateOf(formatDoubles(tConfig.k)) }
|
var tKText by remember(tConfig.k) { mutableStateOf(formatDoubles(tConfig.k)) }
|
||||||
|
|
||||||
var showImportConfirm by remember { mutableStateOf(false) }
|
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 importJson by remember { mutableStateOf<String?>(null) }
|
||||||
var message 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))
|
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 ---
|
// --- About : version installée + lien releases Gitea ---
|
||||||
Card(Modifier.fillMaxWidth()) {
|
Card(Modifier.fillMaxWidth()) {
|
||||||
Column(Modifier.padding(12.dp)) {
|
Column(Modifier.padding(12.dp)) {
|
||||||
|
|||||||
@ -56,6 +56,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import com.hormonetrack.reminder.AlarmScheduler
|
import com.hormonetrack.reminder.AlarmScheduler
|
||||||
import com.hormonetrack.reminder.CalendarEvents
|
import com.hormonetrack.reminder.CalendarEvents
|
||||||
|
import com.hormonetrack.util.AppLog
|
||||||
import com.hormonetrack.ui.LocalAppContainer
|
import com.hormonetrack.ui.LocalAppContainer
|
||||||
import com.hormonetrack.ui.components.formatDose
|
import com.hormonetrack.ui.components.formatDose
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@ -97,7 +98,13 @@ fun TreatmentEditorScreen(treatmentId: Long, onDone: () -> Unit) {
|
|||||||
var calendarPermissionGranted by remember { mutableStateOf(false) }
|
var calendarPermissionGranted by remember { mutableStateOf(false) }
|
||||||
val calendarPermissionLauncher = rememberLauncherForActivityResult(
|
val calendarPermissionLauncher = rememberLauncherForActivityResult(
|
||||||
ActivityResultContracts.RequestMultiplePermissions()
|
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 showDeleteConfirm by remember { mutableStateOf(false) }
|
||||||
var showPresetMenu by remember { mutableStateOf(false) }
|
var showPresetMenu by remember { mutableStateOf(false) }
|
||||||
var showTimePicker by remember { mutableStateOf(false) }
|
var showTimePicker by remember { mutableStateOf(false) }
|
||||||
@ -427,17 +434,18 @@ fun TreatmentEditorScreen(treatmentId: Long, onDone: () -> Unit) {
|
|||||||
Switch(
|
Switch(
|
||||||
checked = calendarEnabled,
|
checked = calendarEnabled,
|
||||||
onCheckedChange = { want ->
|
onCheckedChange = { want ->
|
||||||
if (want && !calendarPermissionGranted) {
|
if (want) {
|
||||||
calendarPermissionLauncher.launch(
|
if (calendarPermissionGranted) {
|
||||||
arrayOf(
|
calendarEnabled = true
|
||||||
android.Manifest.permission.WRITE_CALENDAR,
|
} else {
|
||||||
android.Manifest.permission.READ_CALENDAR
|
// 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 {
|
} else {
|
||||||
calendarEnabled = false
|
calendarEnabled = false
|
||||||
}
|
}
|
||||||
@ -511,8 +519,11 @@ fun TreatmentEditorScreen(treatmentId: Long, onDone: () -> Unit) {
|
|||||||
if (eventToStore != saved.calendarEventId) {
|
if (eventToStore != saved.calendarEventId) {
|
||||||
repo.updateTreatment(saved.copy(calendarEventId = eventToStore))
|
repo.updateTreatment(saved.copy(calendarEventId = eventToStore))
|
||||||
}
|
}
|
||||||
} catch (_: SecurityException) {
|
AppLog.log("agenda",
|
||||||
// permission agenda révoquée entre-temps → ignorer
|
"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() }
|
withContext(Dispatchers.Main) { onDone() }
|
||||||
}
|
}
|
||||||
|
|||||||
68
app/src/main/java/com/hormonetrack/util/AppLog.kt
Normal file
68
app/src/main/java/com/hormonetrack/util/AppLog.kt
Normal 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)
|
||||||
|
}
|
||||||
15
app/src/main/java/com/hormonetrack/util/HrtDuration.kt
Normal file
15
app/src/main/java/com/hormonetrack/util/HrtDuration.kt
Normal 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)
|
||||||
|
}
|
||||||
@ -33,6 +33,13 @@
|
|||||||
<string name="show_extrema">Pics / creux</string>
|
<string name="show_extrema">Pics / creux</string>
|
||||||
<string name="zoom_in">Zoom avant (fenêtre plus courte)</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="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="changelog_title">Nouveautés de la version %1$s</string>
|
||||||
<string name="settings_version">Version installée : %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>
|
<string name="releases_link">Dernières versions sur Gitea (télécharger les APK)</string>
|
||||||
|
|||||||
@ -33,6 +33,13 @@
|
|||||||
<string name="show_extrema">Peaks / troughs</string>
|
<string name="show_extrema">Peaks / troughs</string>
|
||||||
<string name="zoom_in">Zoom in (shorter window)</string>
|
<string name="zoom_in">Zoom in (shorter window)</string>
|
||||||
<string name="zoom_out">Zoom out (longer 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="changelog_title">What\'s new in %1$s</string>
|
||||||
<string name="settings_version">Installed version: %1$s</string>
|
<string name="settings_version">Installed version: %1$s</string>
|
||||||
<string name="releases_link">Latest releases on Gitea (download APKs)</string>
|
<string name="releases_link">Latest releases on Gitea (download APKs)</string>
|
||||||
|
|||||||
37
app/src/test/java/com/hormonetrack/util/AppLogTest.kt
Normal file
37
app/src/test/java/com/hormonetrack/util/AppLogTest.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
38
app/src/test/java/com/hormonetrack/util/HrtDurationTest.kt
Normal file
38
app/src/test/java/com/hormonetrack/util/HrtDurationTest.kt
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,27 @@
|
|||||||
# Changelog — HormoneTrack
|
# Changelog — HormoneTrack
|
||||||
|
|
||||||
Format : [Keep a Changelog](https://keepachangelog.com/fr-FR/1.1.0/).
|
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)
|
## [1.3.0] — 2026-09-06 (versionCode 14)
|
||||||
|
|
||||||
|
|||||||
@ -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.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.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. |
|
| 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 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. |
|
| 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. |
|
| 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
|
## 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.
|
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 =
|
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/…`).
|
`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
|
(1.2.9 < 1.2.10 : la comparaison lexicographique serait fausse), extraction
|
||||||
de sections depuis la dernière vue, première installation (section courante
|
de sections depuis la dernière vue, première installation (section courante
|
||||||
seule), plusieurs versions intermédiaires, versions non numériques.
|
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`,
|
- **`CalendarRruleTest`** (3) : v1.3.0 — RRULE `FREQ=DAILY;INTERVAL=N`,
|
||||||
arrondi demi-supérieur des décimales (6,5 → 7), clamp ≥ 1 jour.
|
arrondi demi-supérieur des décimales (6,5 → 7), clamp ≥ 1 jour.
|
||||||
- **`ExtremaTest`** (6) : v1.2.3 — détection des pics/creux (`detectExtrema`) :
|
- **`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)
|
0,02 ng/mL)
|
||||||
- `DosesScreen` : LazyColumn par jour (desc), **Δ jours depuis la dose précédente du même
|
- `DosesScreen` : LazyColumn par jour (desc), **Δ jours depuis la dose précédente du même
|
||||||
traitement** (`intervalsByDoseId`, colonne « Interval (d) » du `.ods`), suppression
|
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 :
|
- `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
|
deux sections optionnelles → 1 ou 2 LabResult au même timestamp) ; affichage
|
||||||
**groupé par timestamp** (`groupLabsForDisplay` : E2 avant T, autres ensuite, tri
|
**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 +
|
switch « Simuler les doses à venir » + intervalle en jours)** ; carte Rappel (switch +
|
||||||
TimePicker **centré** + avertissement alarmes exactes) ; switch actif ; save →
|
TimePicker **centré** + avertissement alarmes exactes) ; switch actif ; save →
|
||||||
insert/update + schedule/cancel ; delete avec confirmation ; `createdAt` préservé
|
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`
|
- **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`),
|
est plus récente que la dernière vue (DataStore `changelog_seen_version`),
|
||||||
un AlertDialog affiche les sections CHANGELOG concernées (extraites par
|
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
|
PATCH de renommage peuvent être ignorés (asset au nom générique, APK
|
||||||
release disparu de la release). → `ensure_asset` dans le script :
|
release disparu de la release). → `ensure_asset` dans le script :
|
||||||
upload + vérification nom/taille + retry PATCH + échec bruyant.
|
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
|
38. **Comparaison lexicographique de tags/versions** (v1.3.0, publication
|
||||||
farewell) : `tag >= "v1.2.5"` en comparaison de CHAÎNES fait `v1.2.10 <
|
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. →
|
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
|
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.*
|
||||||
|
|||||||
@ -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
|
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 —
|
rappels, mais **son historique reste simulé et calibré** sur les graphiques —
|
||||||
parfait pour une transition valerate → enanthate
|
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`
|
- 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)
|
- Pour gel/patch/oral : paramètres Bateman (temps au pic, demi-vie, biodispo)
|
||||||
2. **Doses → +** → logue tes injections passées
|
2. **Doses → +** → logue tes injections passées
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user