v1.3.4 : 2 crashs v1.3.3 reproduits sur émulateur (données réelles) et corrigés — #47 Doses : stringResource hrt_duration 3 placeholders vs 2 args (né v1.3.1) ; #48 export logs : LocalDate.format('…-HHmm') levait UnsupportedTemporalTypeException au tap → util ExportFileNames + 3 tests ; #49 notif dose StringFormatMatches ; lint filet bloquant (app/lint.xml) ; recette émulateur §16.ter + scripts/seed-emulator.py ; 90 tests verts

This commit is contained in:
Siphonight 2026-09-06 12:38:57 +02:00
parent e5fefc90df
commit 71659b5647
12 changed files with 446 additions and 32 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 > à 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.3 — build Android ✅, **87 tests unitaires** ✅ (44 sans les données de test locales ; 3 régressions épinglées sur données réelles **non versionnées**), intégration montre = notifications ✅, **dépôt Gitea privé + releases avec APK** ✅ - **Statut** : v1.3.4 — build Android ✅, **lint vert** ✅, **90 tests unitaires** ✅ (72 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)
@ -73,7 +73,8 @@ par AGP si les licences sont signées). Le wrapper télécharge Gradle 9.7.1.
git clone <repo> && cd HormoneTrack git clone <repo> && cd HormoneTrack
echo "sdk.dir=/chemin/vers/android-sdk" > local.properties # ou ANDROID_HOME echo "sdk.dir=/chemin/vers/android-sdk" > local.properties # ou ANDROID_HOME
./gradlew assembleDebug # APK : app/build/outputs/apk/debug/app-debug.apk ./gradlew assembleDebug # APK : app/build/outputs/apk/debug/app-debug.apk
./gradlew testDebugUnitTest # 87 tests (44 sans les données locales) ./gradlew testDebugUnitTest # 90 tests (72 sans les données locales)
./gradlew lint # lint vert obligatoire avant release
``` ```
Installation sur un téléphone : mode développeur + Débogage USB, puis Android Studio Installation sur un téléphone : mode développeur + Débogage USB, puis Android Studio
@ -84,7 +85,7 @@ Pas de Play Store : l'app est sideloadée. Détails pas-à-pas : [docs/GUIDE_INS
Dépôts : **gitea.cloudyfy.fr** et **gitea.farewell.dev** (miroir) — Dépôts : **gitea.cloudyfy.fr** et **gitea.farewell.dev** (miroir) —
`Siphonight/HormoneTrack` sur les deux (privé), avec `Siphonight/HormoneTrack` sur les deux (privé), avec
**releases taguées** (`v1.1.0` → `v1.3.3`) et **deux APK par release** (depuis v1.2.5) **releases taguées** (`v1.1.0` → `v1.3.4`) et **deux APK par release** (depuis v1.2.5)
(téléchargeables sans compiler, cf [docs/DEVELOPPEMENT.md §16.bis](docs/DEVELOPPEMENT.md)) : (téléchargeables sans compiler, cf [docs/DEVELOPPEMENT.md §16.bis](docs/DEVELOPPEMENT.md)) :
`-release.apk` (**recommandé**, optimisé R8, 2,4 Mo) et `-debug.apk` (20 Mo) : `-release.apk` (**recommandé**, optimisé R8, 2,4 Mo) et `-debug.apk` (20 Mo) :
@ -140,7 +141,9 @@ HormoneTrack/
│ ├── CHANGELOG.md journal détaillé des versions │ ├── CHANGELOG.md journal détaillé des versions
│ └── MONTRE-GADGETBRIDGE.md montre Huawei GT 3 : options + limites │ └── MONTRE-GADGETBRIDGE.md montre Huawei GT 3 : options + limites
├── scripts/ ├── scripts/
│ └── gitea-release.py publie une release (corps = CHANGELOG + APK) │ ├── gitea-release.py publie le corps d'une release (CHANGELOG + APK)
│ ├── publish-release.py publie les 2 APK d'un tag (vérification par téléchargement)
│ └── seed-emulator.py injecte un backup JSON dans la DB d'un émulateur (cf §16.ter)
├── local-test-data/ ← gitignoré : backups réels pour les tests ├── local-test-data/ ← gitignoré : backups réels pour les tests
│ de régression (JAMAIS dans le dépôt, cf §8.bis) │ de régression (JAMAIS dans le dépôt, cf §8.bis)
├── build.gradle.kts config Gradle racine (AGP/Kotlin/KSP épinglés) ├── build.gradle.kts config Gradle racine (AGP/Kotlin/KSP épinglés)

View File

@ -17,8 +17,8 @@ android {
// tag : v1.3.0→v1.3.2 contenaient tous versionCode 14 / "1.3.0" // tag : v1.3.0→v1.3.2 contenaient tous versionCode 14 / "1.3.0"
// (bump jamais commité) → BuildConfig.VERSION_NAME était faux dans // (bump jamais commité) → BuildConfig.VERSION_NAME était faux dans
// les APK publiés (Paramètres + titre du dialog « Nouveautés »). // les APK publiés (Paramètres + titre du dialog « Nouveautés »).
versionCode = 17 versionCode = 18
versionName = "1.3.3" versionName = "1.3.4"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
@ -61,6 +61,15 @@ android {
excludes += "/META-INF/{AL2.0,LGPL2.1}" excludes += "/META-INF/{AL2.0,LGPL2.1}"
} }
} }
// Lint (v1.3.4) : les checks Compose 1.12+ (NonObservableLocale,
// LocalContextGetResourceValueCall) sont rétrogradés en WARNING via
// app/lint.xml (meilleure compatibilité que la DSL AGP) — cas THÉORIQUES de staleness de
// configuration présents dans tout le code v1, jamais à l'origine des
// crashs remontés. Les vrais problèmes de format de strings
// (StringFormatMatches — aurait attrapé les bugs #47/#48, épinglés §14)
// restent des ERREURS bloquantes. Re-traiter ces warnings lors d'une
// refonte i18n (cf §20).
} }
// Synchronise docs/CHANGELOG.md → src/main/assets/changelog.md (dialog // Synchronise docs/CHANGELOG.md → src/main/assets/changelog.md (dialog

20
app/lint.xml Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Lint (/v1.3.4). Cf commentaires dans app/build.gradle.kts et §19 de la doc de dev.
Rétrogradation des deux checks Compose 1.12+ : signalements de staleness
de configuration (lecture de resources via LocalContext dans les callbacks
et coroutines, Locale.getDefault() dans un remember{}) — présents dans tout
le code v1, jamais à l'origine des crashs remontés. À re-traiter lors d'une
refonte i18n (cf docs/DEVELOPPEMENT.md §20).
RESTENT EN ERREUR (bloquants, aucun override nécessaire) :
- StringFormatMatches / StringFormatCount / StringFormatInvalid :
auraient attrapé les bugs #47 (hrt_duration : 3 placeholders vs 2 args
passés) et #48 (pattern horaire formaté sur LocalDate) — crashs réels
v1.3.1→v1.3.3 retrouvé au téléphone, cf §14.
-->
<lint>
<issue id="NonObservableLocale" severity="warning" />
<issue id="LocalContextGetResourceValueCall" severity="warning" />
</lint>

View File

@ -182,8 +182,11 @@ class ReminderReceiver : BroadcastReceiver() {
} else { } else {
context.getString(R.string.reminder_title_plain) context.getString(R.string.reminder_title_plain)
} }
// ⚠️ reminder_text_with_dose attend un %s : passer le Double brut
// déclenche un warning lint StringFormatMatches (« suspicious type ») —
// formater explicitement en dose humaine (ex. « 4,0 »).
val text = if (dose != null && dose > 0.0) { val text = if (dose != null && dose > 0.0) {
context.getString(R.string.reminder_text_with_dose, dose) context.getString(R.string.reminder_text_with_dose, dose.toString())
} else { } else {
context.getString(R.string.reminder_text) context.getString(R.string.reminder_text)
} }

View File

@ -122,8 +122,13 @@ fun DosesScreen() {
val totalDays = HrtDuration.daysSince(first, System.currentTimeMillis()) val totalDays = HrtDuration.daysSince(first, System.currentTimeMillis())
if (totalDays > 0) { if (totalDays > 0) {
val (months, days) = HrtDuration.monthsAndDays(totalDays) val (months, days) = HrtDuration.monthsAndDays(totalDays)
// ⚠️ hrt_duration a TROIS placeholders (%1$d mois, %2$d jours,
// %3$d total) : passer aussi totalDays, sinon
// MissingFormatArgumentException = crash de l'écran Doses
// dès que totalDays > 0 (donc à chaque affichage avec des
// données réelles). Testé au bug réel v1.3.3 (cf §14 #47).
Text( Text(
if (months > 0) stringResource(R.string.hrt_duration, months, days) if (months > 0) stringResource(R.string.hrt_duration, months, days, totalDays)
else stringResource(R.string.hrt_duration_days_only, days), else stringResource(R.string.hrt_duration_days_only, days),
style = MaterialTheme.typography.bodyLarge style = MaterialTheme.typography.bodyLarge
) )

View File

@ -52,12 +52,11 @@ 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
import com.hormonetrack.ui.LocalAppContainer import com.hormonetrack.ui.LocalAppContainer
import com.hormonetrack.util.ExportFileNames
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.util.Locale import java.util.Locale
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -362,8 +361,7 @@ fun SettingsScreen(onBack: () -> Unit) {
Spacer(Modifier.height(6.dp)) Spacer(Modifier.height(6.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = { Button(onClick = {
val date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) exportLauncher.launch(ExportFileNames.backupFileName())
exportLauncher.launch("hormonetrack-backup-$date.json")
}) { Text(stringResource(R.string.export_json)) } }) { Text(stringResource(R.string.export_json)) }
OutlinedButton(onClick = { OutlinedButton(onClick = {
importLauncher.launch(arrayOf("application/json")) importLauncher.launch(arrayOf("application/json"))
@ -385,8 +383,10 @@ fun SettingsScreen(onBack: () -> Unit) {
Spacer(Modifier.height(6.dp)) Spacer(Modifier.height(6.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedButton(onClick = { OutlinedButton(onClick = {
val date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmm")) // Nom de fichier via ExportFileNames (cf §14 #48) :
logExportLauncher.launch("hormonetrack-logs-$date.txt") // extrait et testé — passer un LocalDate au pattern
// horaire crashait l'app au tap (bug v1.3.1→v1.3.3).
logExportLauncher.launch(ExportFileNames.diagnosticLogFileName())
}) { Text(stringResource(R.string.logs_export)) } }) { Text(stringResource(R.string.logs_export)) }
TextButton(onClick = { TextButton(onClick = {
AppLog.clear() AppLog.clear()

View File

@ -0,0 +1,46 @@
package com.hormonetrack.util
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
/**
* Noms de fichiers d'export (horodatés) — helper PUR, testable en JVM.
*
* Pourquoi cet objet existe (v1.3.4, cf §14 #48) : le nom du fichier des logs
* était généré INLINE dans le handler du bouton via
* `LocalDate.now().format(ofPattern("yyyyMMdd-HHmm"))`. Un `LocalDate` n'a
* PAS de champ horaire : le pattern `HHmm` levait
* `UnsupportedTemporalTypeException: Unsupported field: HourOfDay` de façon
* SYNCHRONE dans le onClick (thread UI) → crash de l'app au tap, sans même
* ouvrir le sélecteur de fichiers. Crash présent depuis v1.3.1 ; les « fixes »
* v1.3.2 et v1.3.3 réparaient l'IO mais pas cette ligne.
*
* Règle anti-régression : les helpers d'ici centralisent le couplage
* type ↔ pattern, chaque fonction documentant quel type porte le pattern.
* ⚠️ Règle générale : un pattern contenant des champs horaires (HH, mm, ss)
* exige LocalDateTime/LocalTime/Instant.withZone ; yyyyMMdd seul sur LocalDate.
*/
object ExportFileNames {
private val LOG_FMT = DateTimeFormatter.ofPattern("yyyyMMdd-HHmm")
private val BACKUP_FMT = DateTimeFormatter.ofPattern("yyyyMMdd")
/**
* Nom du fichier d'export des logs de diagnostic :
* `hormonetrack-logs-20260906-1234.txt`.
* Le pattern contient HHmm → il faut un type TEMPOREL COMPLET
* (LocalDateTime). Passer un LocalDate ici = UnsupportedTemporalTypeException
* au tap sur « Exporter » (bug #48, crash réel v1.3.1→v1.3.3).
*/
fun diagnosticLogFileName(now: LocalDateTime = LocalDateTime.now()): String =
LOG_FMT.format(now).let { "hormonetrack-logs-$it.txt" }
/**
* Nom du fichier de sauvegarde JSON :
* `hormonetrack-backup-20260906.json`. Pattern SANS heure →
* `LocalDate` est correct ici (c'est le cas qui marchait, à garder tel quel).
*/
fun backupFileName(today: LocalDate = LocalDate.now()): String =
"hormonetrack-backup-${BACKUP_FMT.format(today)}.json"
}

View File

@ -0,0 +1,47 @@
package com.hormonetrack.util
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import java.time.LocalDate
import java.time.LocalDateTime
/**
* Régression du bug #48 (v1.3.4) : le nom de fichier des logs était formatté
* `LocalDate.now().format(ofPattern("yyyyMMdd-HHmm"))` → TypeError
* UnsupportedTemporalTypeException: Unsupported field: HourOfDay
* levée SYNCHRONEMENT dans le onClick du bouton « Exporter » des logs
* (thread UI) → crash de l'app au tap, avant même le sélecteur de fichiers.
* Préalablement invisible en tests JVM : la logique vivait inline dans la
* SettingsScreen (composable). Le helper est désormais pur et testé ici.
*/
class ExportFileNamesTest {
@Test
fun `log filename is built from a LocalDateTime (has a time field)`() {
val name = ExportFileNames.diagnosticLogFileName(
LocalDateTime.of(2026, 9, 6, 12, 34)
)
assertEquals("hormonetrack-logs-20260906-1234.txt", name)
}
@Test
fun `backup filename is date-only (LocalDate is valid for yyyyMMdd)`() {
val name = ExportFileNames.backupFileName(
LocalDate.of(2026, 9, 6)
)
assertEquals("hormonetrack-backup-20260906.json", name)
}
@Test
fun `default filenames never throw and match the expected shape`() {
assertTrue(
ExportFileNames.diagnosticLogFileName()
.matches(Regex("hormonetrack-logs-\\d{8}-\\d{4}\\.txt"))
)
assertTrue(
ExportFileNames.backupFileName()
.matches(Regex("hormonetrack-backup-\\d{8}\\.json"))
)
}
}

View File

@ -1,7 +1,51 @@
# 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.3`, …). Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.3.4`, …).
## [1.3.4] — 2026-09-06 (versionCode 18)
### Corrigé
- **L'écran Doses faisait crasher l'app** (remontée) : `MissingFormatArgumentException:
Format specifier '%3$d'` — la string `hrt_duration` a TROIS placeholders
(`%1$d mois, %2$d jours, %3$d total`) mais l'appel transmettait 2 arguments.
Crash dès le premier affichage de l'écran Doses avec une donnée antérieure à
aujourd'hui. **Le bug est né en v1.3.1** (string + appel dans le même commit,
jamais testés ensemble) et a survécu aux releases v1.3.1→v1.3.3. Fix : passer
totalDays en 3ᵉ argument (cf §14 #47).
- **L'export des logs de diagnostic faisait crasher l'app** (remontée répétée ;
les « fixes » IO de v1.3.2 et v1.3.3 étaient corrects mais à côté du vrai
problème) : le nom de fichier était construit via
`LocalDate.now().format(ofPattern("yyyyMMdd-HHmm"))` — un `LocalDate` n'a pas
de champ horaire → `UnsupportedTemporalTypeException: Unsupported field:
HourOfDay`, levée SYNCHRONEMENT dans le onClick (thread UI) au tap sur
« Exporter », avant même l'ouverture du gestionnaire de fichiers. L'export
JSON fonctionnait car son pattern `yyyyMMdd` est valide pour un LocalDate.
FIX : utilitaire `ExportFileNames` (pur, testé) — `diagnosticLogFileName()`
utilise un `LocalDateTime` ; `backupFileName()` reste sur `LocalDate`.
Cf §14 #48.
- **Lint `StringFormatMatches`** (détecté en implantant le lint — nouveau filet
de la v1.3.4) : la notification de rappel passait un Double à une `%s`.
Fix : `dose.toString()` (comportement d'affichage inchangé, #49).
### Ajouté
- **Lint en filet bloquant** : `./gradlew lint` est désormais vert (it aurait
attrapé dès v1.3.1 les deux crashs ci-dessus via `StringFormatMatches`).
Les deux checks Compose 1.12+ théoriques (`NonObservableLocale`,
`LocalContextGetResourceValueCall`) sont rétrogradés en warning via
`app/lint.xml` (staleness de config, refonte i18n plus tard, cf §19/§20).
- **Recette reproductible : test manuel sur émulateur** (documentée §16.ter) :
émulateur + image ATD ou google_apis, seed de la DB du debug avec un backup
JSON via le nouveau script **`scripts/seed-emulator.py`** (générique, sans
aucune donnée embarquée), installation du APK release par-dessus (données
conservées, même signature), pilotage par `uiautomator dump` + `input tap`,
crashs via `adb logcat -b crash`. C'est cette recette qui a reproduit et
confirmé les deux crashs ci-dessus ainsi que LEUR correction (flux SAF
complet : picker → SAVE → fichier écrit → message de confirmation, sur
l'APK release données réelles).
- **3 nouveaux tests** (`ExportFileNamesTest`, 90 au total / 72 sans les
données locales) : épinglent le format des noms de fichiers d'export —
auraient attrapé le bug #48 le jour même.
## [1.3.3] — 2026-09-06 (versionCode 17) ## [1.3.3] — 2026-09-06 (versionCode 17)

View File

@ -80,6 +80,7 @@ Fonctionnalités v1 :
| 6 sept. 2026 (session v1.3.2) | 3 bugs remontés : toggle agenda (callback async fix + ContextCompat au save + Posologie requise), export logs plantait (pattern JSON réutilisé + AppLog), dialog changelog récurrent (version vue mémorisée avant affichage + titre BuildConfig documenté) ; export réel mis à jour (3 traitements : EV inactif + EEn actif + CPA oral) ; **87 tests verts**, APK v1.3.2 + releases. | | 6 sept. 2026 (session v1.3.2) | 3 bugs remontés : toggle agenda (callback async fix + ContextCompat au save + Posologie requise), export logs plantait (pattern JSON réutilisé + AppLog), dialog changelog récurrent (version vue mémorisée avant affichage + titre BuildConfig documenté) ; export réel mis à jour (3 traitements : EV inactif + EEn actif + CPA oral) ; **87 tests verts**, APK v1.3.2 + releases. |
| 6 sept. 2026 (session farewell) | Repo créé côté farewell → **push SSH (alias `farewell` : giteassh:2222) + 12 releases publiées avec APK vérifiés par téléchargement** ; piège lexicographique `v1.2.10 < v1.2.5` en comparaison de chaînes épinglé (§14 #38) ; les deux instances Gitea sont synchrones. | | 6 sept. 2026 (session 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.3) | **Audit de reprise de maintenance** (nouvelle session IA) : 3 bugs racines trouvés — permissions agenda ABSENTES du manifest (jamais déclarées, §14 #44), export logs plantait TOUJOURS (le fix v1.3.2 réimplémentait l'IO au lieu de réutiliser BackupManager, §14 #45), bump de version JAMAIS commité (tags v1.3.0–1.3.2 tous `versionCode 14 / "1.3.0"`, §14 #46). Corrigés + doc rafraîchie (DB v3, targetSdk 36, régression 3 = export v1.3.1, §19/§20 staleness) ; **téléphone de test documenté : Google Pixel 9 /e/OS** (AOSP ; SAF DocumentsUI standard). | | 6 sept. 2026 (session v1.3.3) | **Audit de reprise de maintenance** (nouvelle session IA) : 3 bugs racines trouvés — permissions agenda ABSENTES du manifest (jamais déclarées, §14 #44), export logs plantait TOUJOURS (le fix v1.3.2 réimplémentait l'IO au lieu de réutiliser BackupManager, §14 #45), bump de version JAMAIS commité (tags v1.3.0–1.3.2 tous `versionCode 14 / "1.3.0"`, §14 #46). Corrigés + doc rafraîchie (DB v3, targetSdk 36, régression 3 = export v1.3.1, §19/§20 staleness) ; **téléphone de test documenté : Google Pixel 9 /e/OS** (AOSP ; SAF DocumentsUI standard). |
| 6 sept. 2026 (session v1.3.4) | **Deux crashs v1.3.3 remontés (Doses + export logs)** → reproduits sur ÉMULATEUR avec les vraies données (recette §16.ter, désormais standard) : (a) Doses = string `hrt_duration` 3 placeholders vs 2 args (**né en v1.3.1**, §14 #47) ; (b) export logs = `LocalDate.format("…-HHmm")` levant `UnsupportedTemporalTypeException` au TAP (§14 #48) — les fixes IO v1.3.2/v1.3.3 étaient à côté du vrai problème. Lint mis en filet bloquant (aurait attrapé les deux) + #49 notif ; helper `ExportFileNames` + 3 tests (90 verts) ; fixes VÉRIFIÉS sur émulateur avec l'APK release + données réelles (flux SAF complet : SAVE → fichier + message). Publication v1.3.4. |
| 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. |
| 5 sept. 2026 (session v1.2.9) | **Zoom du graphique** (pinch + boutons, 6 h → 300 j, focal stable, échantillonnage adaptatif `stepForRange`, labels X 1 h/3 h) ; **README : disclaimer IA-assisté** en en-tête ; 3 tests ; **67 tests verts**, APK v1.2.9 + releases. | | 5 sept. 2026 (session v1.2.9) | **Zoom du graphique** (pinch + boutons, 6 h → 300 j, focal stable, échantillonnage adaptatif `stepForRange`, labels X 1 h/3 h) ; **README : disclaimer IA-assisté** en en-tête ; 3 tests ; **67 tests verts**, APK v1.2.9 + releases. |
@ -136,6 +137,15 @@ Notes importantes (v1.2.0) :
par `adb install` ou APK direct. Les retours utilisateur (bugs, exports JSON) viennent par `adb install` ou APK direct. Les retours utilisateur (bugs, exports JSON) viennent
de ce téléphone. (Historique : la montre est une Huawei Watch GT 3 via Gadgetbridge, de ce téléphone. (Historique : la montre est une Huawei Watch GT 3 via Gadgetbridge,
cf §17 — ce n'est PAS le téléphone.) cf §17 — ce n'est PAS le téléphone.)
- **Émulateur local (installé en v1.3.4)** : SDK brew =
`/opt/homebrew/share/android-commandlinetools`. Packages installés :
`emulator` 37.1.11 + `system-images;android-31;aosp_atd;arm64-v8a` (ATD : léger,
boot rapide, MAIS DocumentsUI STUBBÉ (`fakesystemapp`) → SAF non pilotable) et
`system-images;android-36;google_apis;arm64-v8a` (image complète — REQUISE pour
tester les flux SAF). AVD `hrt` (ATD 31) et `hrt36` (API 36) créés via
avdmanager (erreur béninie « devices.xml » au create : l'AVD est créé quand même).
Boot headless : `emulator -avd hrt36 -no-window -no-audio -no-boot-anim -no-snapshot
-gpu swiftshader_indirect`. Recette de test → **§16.ter**.
- **SDK Android** : installé via `brew install --cask android-commandlinetools` - **SDK Android** : installé via `brew install --cask android-commandlinetools`
→ `/opt/homebrew/share/android-commandlinetools` (524 MB) → `/opt/homebrew/share/android-commandlinetools` (524 MB)
- licences acceptées : `yes | sdkmanager --licenses` - licences acceptées : `yes | sdkmanager --licenses`
@ -361,8 +371,9 @@ Type de retour : `LevelPoint(timestamp, e2, t)`.
## 8. Tests unitaires ## 8. Tests unitaires
**87 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — 44 sans les **87 tests JVM, tous verts** → **90 depuis v1.3.4** (`./gradlew testDebugUnitTest`) —
données de test locales (cf §8.bis). Dépendance : JUnit 4.13.2. **72 sans les données de test locales** (cf §8.bis : les 3 classes de régression,
6 tests chacune, sont skippées via `Assume`). 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/…`).
@ -837,6 +848,37 @@ aucun contexte, dans l'esprit de cette doc) :**
AVANT le tag ; le bump fait partie du commit de release (jamais un AVANT le tag ; le bump fait partie du commit de release (jamais un
état local non commité au moment du build). état local non commité au moment du build).
**Session v1.3.4 (deux crashs remontés → reproduits sur émulateur) :**
47. **L'écran Doses crashait l'app** (`MissingFormatArgumentException:
Format specifier '%3$d'`) : la string `hrt_duration` a TROIS placeholders
(`%1$d mois, %2$d jours, %3$d total`) mais
`stringResource(R.string.hrt_duration, months, days)` ne passait que DEUX
arguments. Crash dès que le fragment `totalDays > 0` s'affiche (donc pour
TOUTE donnée antérieure à aujourd'hui) — né en v1.3.1 (string + appel dans
le même commit fa5d2df, jamais testés ensemble) et passé à travers
v1.3.1→v1.3.3.
48. **Le bouton « Exporter » des logs crashait l'app** — LE bug récurrent des
v1.3.1→v1.3.3 : le nom de fichier était construit INLINE via
`LocalDate.now().format(ofPattern("yyyyMMdd-HHmm"))`. Un `LocalDate` n'a
PAS de champ horaire → `UnsupportedTemporalTypeException:
Unsupported field: HourOfDay` levée dans le onClick (thread principal,
synchrone, touch dispatch) → crash au tap, avant même le sélecteur SAF
(stack obtenue sur émulateur : LTSI + frames R8 obfusquées). L'export
JSON marchait car `yyyyMMdd` est un pattern valide pour LocalDate.
Zones d'ombre emboîtées : v1.3.2 et v1.3.3 « corrigent » l'IO (cf #41/#45)
sans voir cette ligne. FIX : `util/ExportFileNames.kt` (pur, testé,
documenté — couplage type↔pattern centralisé) ; `FileNamesTest` épinglant
les deux helpers (aurait attrapé le bug le jour même). Leçon : la
génération de nom de fichier ne doit JAMAIS vivre inline dans un onClick.
49. **StringFormatMatches** (capture du lint, v1.3.4) : la notification de
rappel passait un Double à `%s`. Fix `dose.toString()` (l'affichage ne
change pas). L'intérêt des règles lint est confirmé PAR CETTE SESSION :
`./gradlew lint` aurait signalé #47 dès v1.3.1 → le lint fait désormais
partie de la vérification avant release (les checks Compose 1.12+
théoriques `NonObservableLocale`/`LocalContextGetResourceValueCall` sont
tombés en warning via `app/lint.xml`, cf §19).
**Leçons** : (a) ne jamais croire un build « probablement bon » sans l'avoir lancé ; **Leçons** : (a) ne jamais croire un build « probablement bon » sans l'avoir lancé ;
(b) les tests sémantiques attrapent ce que la compilation ne voit pas ; (c) se méfier des (b) les tests sémantiques attrapent ce que la compilation ne voit pas ; (c) se méfier des
constantes stdlib « de mémoire » (`ln2`), des mélanges Float/Double, et des APIs M3 constantes stdlib « de mémoire » (`ln2`), des mélanges Float/Double, et des APIs M3
@ -852,10 +894,15 @@ perd les garde-fous acquis à l'usage (cf #45) ; (j) vérifier les POSTULATS dan
code réel, pas dans la doc (« le manifest contient WRITE_CALENDAR », cf #44) ; code réel, pas dans la doc (« le manifest contient WRITE_CALENDAR », cf #44) ;
(k) un bump de version non commité = métadonnées fausses dans les APK publiés (k) un bump de version non commité = métadonnées fausses dans les APK publiés
(cf #46) — le bump fait partie du commit de release ; (l) le scan de (cf #46) — le bump fait partie du commit de release ; (l) le scan de
confidentialité s'ADAPTE aux nouveaux exports (`local-test-data/`) : se régénère confidentialité s'ADAPTE automatiquement aux nouveaux exports
donc à CHECK chaque push — un historique « propre hier » peut devenir hit dès (`local-test-data/`) : un historique « propre hier » peut devenir hit dès
qu'un nouvel export introduit des motifs qui collent ; juger les hits avec qu'un nouvel export introduit des motifs qui collent — re-scanner PUIS juger
l'utilisatrice (cf dette documentée §8.bis). avec l'utilisatrice (dette déjà jugée, cf §8.bis) ; (m) **un crash sans stack
trace = d'abord le reproduire** (émulateur + données réelles, §16.ter) — les
deux « fixes » aveugles de v1.3.2/v1.3.3 (#41/#45) ont laissé passer un bug
trivial (#48) seulement visible sur l'émulateur ; (n) `./gradlew lint` fait
désormais partie de la vérification avant release : `StringFormatMatches`
aurait signalé #47 dès v1.3.1.
## 15. Comment régénérer l'asset pk_profiles.json ## 15. Comment régénérer l'asset pk_profiles.json
@ -980,8 +1027,11 @@ vérifie pas en tests JVM).
1. **Bumper la version** dans `app/build.gradle.kts` : `versionCode = N+1`, 1. **Bumper la version** dans `app/build.gradle.kts` : `versionCode = N+1`,
`versionName = "X.Y.Z+1"` (SemVer : fix = Z, feature = Y). `versionName = "X.Y.Z+1"` (SemVer : fix = Z, feature = Y).
2. **Tests verts obligatoires** : `./gradlew testDebugUnitTest` — 87 au total, 2. **Tests verts obligatoires** : `./gradlew testDebugUnitTest` — 90 au total
44 si `local-test-data/` est absent (les régressions réelles sont skippées). (v1.3.4), 72 si `local-test-data/` est absent (les 3 classes de régression
réelles sont skippées via `Assume`, 6 tests chacune) ; **lint vert
obligatoire** : `./gradlew lint` (v1.3.4 — `StringFormatMatches` aurait
attrapé les crashs #47/#48 dès v1.3.1).
3. **Docs** : section `## [X.Y.Z]` en tête de `docs/CHANGELOG.md` (le corps des 3. **Docs** : section `## [X.Y.Z]` en tête de `docs/CHANGELOG.md` (le corps des
releases Gitea en sera extrait automatiquement par le script), + §14 si bug releases Gitea en sera extrait automatiquement par le script), + §14 si bug
corrigé, + §2 (historique) si notable. corrigé, + §2 (historique) si notable.
@ -1012,10 +1062,83 @@ Notes :
de santé dans le dépôt, cf §8.bis). de santé dans le dépôt, cf §8.bis).
- Téléphone : mode développeur + Débogage USB (détails : GUIDE_INSTALLATION.md) - Téléphone : mode développeur + Débogage USB (détails : GUIDE_INSTALLATION.md)
- À ma charge (assistant) : build + tests JVM ✓ ; émulateur possible sur demande ; - À ma charge (assistant) : build + tests JVM ✓ ; **émulateur installé et
opérationnel depuis v1.3.4 (recette §16.ter)** ;
**les tests humains sur vrai téléphone restent la référence** **les tests humains sur vrai téléphone restent la référence**
(notifs → montre, UX de saisie, pickers, panoramique du chart) (notifs → montre, UX de saisie, pickers, panoramique du chart)
### 16.ter Recette : test manuel sur émulateur (reproductible — v1.3.4)
But : reproduire/valider un crash ou une UI sur l'app RÉELLE (sans téléphone
branché) — c'est cette recette qui a diagnostiqué les crashs #47/#48 en une
session. ⚠️ AUCUNE donnée personnelle n'est embarquée : le backup utilisé vit
sous `local-test-data/` (gitignoré, §8.bis) et est passé au script en argument.
**0. Installation (one-shot, cf §4)** : `sdkmanager "emulator"
"system-images;android-31;aosp_atd;arm64-v8a"` (léger, pour le LOGIQUE sans
SAF) et/ou `"system-images;android-36;google_apis;arm64-v8a"` (image COMPLÈTE,
nécessaire pour piloter les sélecteurs de fichiers). AVD :
`avdmanager create avd -n hrt36 -k "system-images;…" -d pixel_6` (l'erreur
béninie « devices.xml » ne bloque pas la création). Boot headless :
`emulator -avd hrt36 -no-window -no-audio -no-boot-anim -no-snapshot
-gpu swiftshader_indirect &` puis `adb wait-for-device` + attente
`getprop sys.boot_completed`.
**1. Seed des « données réelles »** — la DB est modifiable uniquement sur un
APK DEBUGGABLE (`run-as`) :
```bash
adb install app/build/outputs/apk/debug/app-debug.apk
adb shell am start -n com.hormonetrack/.MainActivity # 1er lancement → crée la DB
adb shell am force-stop com.hormonetrack # flush de la DB
adb shell run-as com.hormonetrack cat databases/hormonetrack.db > /tmp/hrt.db
python3 scripts/seed-emulator.py /tmp/hrt.db local-test-data/backup-v1.3.1.json
adb push /tmp/hrt.db /data/local/tmp/hrt.db
adb shell run-as com.hormonetrack cp /data/local/tmp/hrt.db databases/hormonetrack.db
adb shell run-as com.hormonetrack rm -f databases/hormonetrack.db-wal \
databases/hormonetrack.db-shm # WAL périmé sinon ! (checkpoint implicite fait par le seed)
```
Règles du seed (le script les impose) : colonnes = noms de propriétés Kotlin
(Room n'applique pas de snake_case), enums stockés en String — on insère
SEULEMENT des lignes, on ne touche NI au schéma NI à `room_master_table`
(Room valide le schéma à l'ouverture, pas les lignes).
**2. Passer en RELEASE (c'est LE build à tester)** — `install -r` PAR-DESSUS
le debug CONSERVE la DB (les APK release/signés debug sont la même clé) :
```bash
adb install -r app/build/outputs/apk/release/app-release.apk
adb logcat -c && adb shell am start -n com.hormonetrack/.MainActivity
```
**3. Piloter l'UI headless** : à l'aide de `uiautomator dump` + `input tap` —
l'UI se pilote SANS écran :
```bash
adb shell uiautomator dump /sdcard/ui.xml && adb shell cat /sdcard/ui.xml
# → parser text=…/content-desc=… + bounds=[x1,y1][x2,y2] → centre (cx,cy)
adb shell input tap <cx> <cy> # naviguer (5 tabs ≈ y=2280 à 1080×2400)
adb shell input keyevent KEYCODE_BACK
```
**4. Attraper le crash** : le process meurt et repasse au launcher — c'est le
signal ; stack : `adb logcat -b crash -d` (grep FATAL). **0 crash attendu :
`grep -c FATAL` reste à 0** après chaque interaction.
**5. Flux SAF (export JSON/logs)** : sur android-31 ATD le
DocumentsUI est STUBBÉ (`fakesystemapp`) → INUTILISABLE ; utiliser l'image
android-36 google_apis. Le picker se pilote pareil (bouton `SAVE` en bas à
droite) ; vérification : fichier présent dans `/sdcard/Download/` + le message
de confirmation (v1.3.3 : « Logs de diagnostic exportés ») visible via un
nouveau dump UI.
**6. Vérifications post-fix** : Doses (en-tête « temps sous THS » + liste),
Settings → Export logs (SAF + SAVE + message), export/import JSON, changelog
dialog au premier lancement (sinon re-seed un `changelog_seen_version`).
⚠️ Les coordonnées UI (x/y) évoluent avec l'écran/l'app : TOUJOURS re-dumper
`uiautomator` avant de taper — ne jamais figer des coordonnées hors dump.
## 17. Montre : Gadgetbridge & options ## 17. Montre : Gadgetbridge & options
Doc dédiée : [MONTRE-GADGETBRIDGE.md](MONTRE-GADGETBRIDGE.md). Synthèse : Doc dédiée : [MONTRE-GADGETBRIDGE.md](MONTRE-GADGETBRIDGE.md). Synthèse :
@ -1062,6 +1185,11 @@ Volontaires (v1) :
explicites, mais R8 ne se vérifie pas en tests JVM → **smoke-test sur explicites, mais R8 ne se vérifie pas en tests JVM → **smoke-test sur
téléphone** (export/import backup, graphiques) avant chaque publication ; téléphone** (export/import backup, graphiques) avant chaque publication ;
signé clé debug → upgradable sans perte, mais pas une signature « officielle » signé clé debug → upgradable sans perte, mais pas une signature « officielle »
- **Lint (v1.3.4)** : `./gradlew lint` est vert — `StringFormatMatches` et
famille restent des ERREURS bloquantes ; les checks Compose 1.12+
théoriques (`NonObservableLocale`, `LocalContextGetResourceValueCall`,
staleness de configuration) sont rétrogradés en WARNING via `app/lint.xml`
→ à re-traiter lors d'une refonte i18n (§20)
## 20. Idées d'évolution ## 20. Idées d'évolution
@ -1135,11 +1263,25 @@ Sur le téléphone de test (à compléter par l'utilisatrice) :
accord, l'événement récurrent apparaît dans l'app d'agenda du Pixel 9 ; accord, l'événement récurrent apparaît dans l'app d'agenda du Pixel 9 ;
le smoke-test R8 de la checklist §16 doit aussi couvrir export/import le smoke-test R8 de la checklist §16 doit aussi couvrir export/import
de backup + graphiques (réflexion Gson sous R8) de backup + graphiques (réflexion Gson sous R8)
- [ ] **v1.3.4** : **écran Doses** s'ouvre et affiche l'en-tête « temps sous
THS » avec les données réelles (bug #47 : crash dès que des doses
antérieures à aujourd'hui existaient — né v1.3.1)
- [ ] **v1.3.4** : **export des logs** ouvre VRAIMENT le gestionnaire de
fichiers et le message « Logs de diagnostic exportés » apparaît après
la sauvegarde (bug #48 : crash au tap depuis v1.3.1) — vérifié sur
émulateur API 36 (§16.ter), à confirmer au téléphone
- [ ] **v1.3.4** : rappel de notification affiche la dose proprement
(lint #49, ex format « 4.0 »)
- [ ] **v1.3.4** : `./gradlew lint` vert — intégré à la vérification de
release (step 2 de la checklist §16 désormais : tests + lint)
--- ---
*Doc mise à jour le 6 sept. 2026 (v1.3.3) — build OK, 87/87 tests verts (44 sans les *Doc mise à jour le 6 sept. 2026 (v1.3.4) — build OK, lint vert, 90/90 tests verts (72 sans
données locales), dépôts Gitea (cloudyfy + farewell) avec releases APK, aucune donnée 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. v1.3.3 = session de reprise de maintenance : de santé dans le dépôt ni l'historique. v1.3.3 = session de reprise de maintenance :
3 bugs racines corrigés (permissions agenda, export logs, bump de version) ; doc 3 bugs racines corrigés (permissions agenda, export logs, bump de version). v1.3.4 :
raccourcie (DB v3, targetSdk 36, régression 3 = export v1.3.1, numéros #38/#43 rétablis, 2 crashs remontés reproduits sur émulateur avec les données réelles (recette §16.ter,
staleness §19/§20 nettoyé).* scripts/seed-emulator.py versionné) — bugs #47 (stringResource arity, né v1.3.1) et #48
(LocalDate + pattern horaire au tap), lint mis en filet bloquant (#49, app/lint.xml),
ExportFileNames + 3 tests. Dette connue : fragments de labs dans l'historique git
(v1.1.0→v1.2.3) — cf §8.bis.*

View File

@ -108,8 +108,11 @@ Les notifications de l'app remontent automatiquement sur la montre via **Huawei
- Choisis bien l'unité : elle est convertie automatiquement pour l'affichage et la - Choisis bien l'unité : elle est convertie automatiquement pour l'affichage et la
calibration (E2 en pg/mL ; T en ng/mL, ng/dL, ng/L, nmol/L) calibration (E2 en pg/mL ; T en ng/mL, ng/dL, ng/L, nmol/L)
4. **Calibration** (dans l'édition d'un traitement E2) → « Calibrer avec les analyses » 4. **Calibration** (dans l'édition d'un traitement E2) → « Calibrer avec les analyses »
- Paramètres → **Logs de diagnostic** : exporte le journal de l'app (utile - Paramètres → **Logs de diagnostic** : « Exporter » ouvre le gestionnaire de
en cas de bug à signaler) fichiers → choisis où enregistrer le .txt → un message de confirmation
(ou d'erreur) s'affiche en haut de Paramètres — utile pour joindre les
logs à un rapport de bug (≥ v1.3.4 ; les versions 1.3.1–1.3.3
faisaient planter ce bouton)
→ calcule le facteur d'échelle = médiane(lab ÷ prédiction), comme le « Scale factor » du `.ods` → calcule le facteur d'échelle = médiane(lab ÷ prédiction), comme le « Scale factor » du `.ods`
5. **Paramètres** : 5. **Paramètres** :
- **Langue** : Système / Français / English - **Langue** : Système / Français / English
@ -164,6 +167,7 @@ Les profils viennent de ta feuille `Estrogen.ods` (table « Models ») :
| Notif absente sur la montre | Huawei Santé → Notifications → autorise l'app ; redémarre la montre | | Notif absente sur la montre | Huawei Santé → Notifications → autorise l'app ; redémarre la montre |
| Rappels en retard | Paramètres → « Accorder les alarmes exactes » + désactive l'optimisation de batterie pour l'app | | Rappels en retard | Paramètres → « Accorder les alarmes exactes » + désactive l'optimisation de batterie pour l'app |
| Import JSON échoué | Le fichier doit venir d'un export de l'app même version (IDs conservés) | | Import JSON échoué | Le fichier doit venir d'un export de l'app même version (IDs conservés) |
| L'export des logs ou JSON a planté (versions ≤ 1.3.3) | Mettre à jour vers la v1.3.4+ (fixons confirmés sur émulateur) — le message de confirmation doit apparaître en haut de Paramètres |
## 10. Données & vie privée ## 10. Données & vie privée

91
scripts/seed-emulator.py Executable file
View File

@ -0,0 +1,91 @@
#!/usr/bin/env python3
"""Seed la DB Room d'un émulateur avec un backup JSON exporté par l'app.
OUTIL DE TEST MANUEL (post-v1.3.4) — cf recette complète dans
docs/DEVELOPPEMENT.md §16.ter « Test manuel sur émulateur ».
Constitue l'étape « données réelles dans l'émulateur » : lance l'APK debug une
fois (crée la DB via Room), récupère la DB via run-as, injecte les lignes du
backup passé en argument (SANS toucher au schéma ni à room_master_table — Room
valide le schéma, pas les lignes), puis la repousse.
Usage (le téléphone = émulateur démarré, avec com.hormonetrack - APK DEBUG -
installé et lancé au moins une fois) :
adb shell am force-stop com.hormonetrack
adb shell run-as com.hormonetrack cat databases/hormonetrack.db > /tmp/hrt.db
python3 scripts/seed-emulator.py /tmp/hrt.db <backup.json> # modifie la BD locale
adb push /tmp/hrt.db /data/local/tmp/hrt.db
adb shell run-as com.hormonetrack cp /data/local/tmp/hrt.db databases/hormonetrack.db
adb shell run-as com.hormonetrack rm -f databases/hormonetrack.db-wal databases/hormonetrack.db-shm
⚠️ PRIVATITÉ : le fichier backup contient des données de santé personnelles et
vit sous `local-test-data/` (gitignoré, cf §8.bis). Ce script ne contains
AUCUNE donnée : tout passe par arguments/lignes insérées à l'exécution. Ne
JAMAIS copier un backup dans le dépôt ni en nommer dans les commentaires.
"""
import json
import sqlite3
import sys
def main():
if len(sys.argv) != 3:
sys.exit("usage: seed-emulator.py <db-sqlite> <backup.json>")
db_path, backup_path = sys.argv[1], sys.argv[2]
data = json.load(open(backup_path, encoding="utf-8"))
con = sqlite3.connect(db_path)
cur = con.cursor()
# ⚠️ Les colonnes portent les noms des propriétés Kotlin SANS renommage
# (Room ne convertit pas en snake_case) ; les enums sont stockés en String.
for tr in data["treatments"]:
cur.execute(
"INSERT OR REPLACE INTO treatments (id, name, type, route, doseAmount,"
" doseUnit, isActive, notes, esterType, pkModel, absorptionHours,"
" eliminationHalfLifeHours, bioavailabilityFraction, scaleFactor,"
" forecastIntervalDays, reminderHour, reminderMinute, reminderEnabled,"
" calendarEventId, createdAt)"
" VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
(
tr["id"], tr["name"], tr["type"], tr["route"], tr["doseAmount"],
tr["doseUnit"], tr["isActive"], tr.get("notes"),
tr.get("esterType", "NONE"), tr.get("pkModel", "ESE"),
tr.get("absorptionHours", 4.0),
tr.get("eliminationHalfLifeHours", 24.0),
tr.get("bioavailabilityFraction", 1.0), tr.get("scaleFactor", 1.0),
tr.get("forecastIntervalDays"), tr.get("reminderHour"),
tr.get("reminderMinute"), tr.get("reminderEnabled", False),
tr.get("calendarEventId"), tr["createdAt"],
),
)
for d in data["doseLogs"]:
cur.execute(
"INSERT OR REPLACE INTO dose_logs (id, treatmentId, timestamp,"
" doseAmount, notes, esterType) VALUES (?,?,?,?,?,?)",
(d["id"], d["treatmentId"], d["timestamp"], d["doseAmount"],
d.get("notes"), d.get("esterType")),
)
for l in data["labResults"]:
cur.execute(
"INSERT OR REPLACE INTO lab_results (id, marker, value, unit,"
" timestamp, notes) VALUES (?,?,?,?,?,?)",
(l["id"], l["marker"], l["value"], l["unit"], l["timestamp"],
l.get("notes")),
)
con.commit()
cur.execute(
"SELECT (SELECT COUNT(*) FROM treatments), (SELECT COUNT(*) FROM dose_logs),"
" (SELECT COUNT(*) FROM lab_results)")
t, d, l = cur.fetchone()
print(f"seed OK : {t} traitements, {d} doses, {l} labs")
con.close()
if __name__ == "__main__":
main()