v1.3.3 : 3 bugs racines de l'audit de reprise corrigés — permissions agenda absentes du manifest (#44), export logs réutilise BackupManager.writeBackup (+AppLog fail-safe) (#45), bump de version commité avant le tag (#46) ; doc de dev rafraîchie (DB v3, targetSdk 36, Pixel 9 /e/OS, #38/#43, §19-21)

This commit is contained in:
Siphonight 2026-09-06 11:24:12 +02:00
parent 88c365f4d0
commit cb64f0e57e
9 changed files with 234 additions and 56 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.2 — build Android ✅, **87 tests unitaires** ✅ (44 sans les données de test locales ; 3 régressions épinglées sur données réelles **non versionnées**), intégration montre = notifications ✅, **dépôt Gitea privé + releases avec APK** ✅ - **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** ✅
- **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,7 @@ 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 # 36 tests (moteur PK, profils, backup, régression) ./gradlew testDebugUnitTest # 87 tests (44 sans les données locales)
``` ```
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 +84,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.1`) et **deux APK par release** (depuis v1.2.5) **releases taguées** (`v1.1.0` → `v1.3.3`) 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) :

View File

@ -12,8 +12,13 @@ android {
applicationId = "com.hormonetrack" applicationId = "com.hormonetrack"
minSdk = 26 minSdk = 26
targetSdk = 36 targetSdk = 36
versionCode = 14 // ⚠️ MÉTADONNÉES DE VERSION — bump obligatoire à CHAQUE release
versionName = "1.3.0" // (checklist §16 étape 1 de docs/DEVELOPPEMENT.md), committé AVANT le
// tag : v1.3.0→v1.3.2 contenaient tous versionCode 14 / "1.3.0"
// (bump jamais commité) → BuildConfig.VERSION_NAME était faux dans
// les APK publiés (Paramètres + titre du dialog « Nouveautés »).
versionCode = 17
versionName = "1.3.3"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {

View File

@ -6,6 +6,15 @@
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" /> <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<!-- Événements d'agenda récurrents (v1.3.0, calendrier local « HormoneTrack »).
⚠️ Ces permissions DANGEREUSES doivent être déclarées ICI en plus d'être
demandées à l'exécution par l'éditeur de traitement : sans cette
déclaration la demande runtime est refusée d'office et CalendarEvents
lève SecurityException → la fonctionnalité agenda était inopérante
depuis v1.3.0 (cause racine manquante, cf §14 #44 de la doc de dev). -->
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
<uses-permission android:name="android.permission.READ_CALENDAR" />
<application <application
android:name=".HormoneTrackApp" android:name=".HormoneTrackApp"
android:allowBackup="false" android:allowBackup="false"

View File

@ -78,33 +78,56 @@ fun SettingsScreen(onBack: () -> Unit) {
var showImportConfirm by remember { mutableStateOf(false) } var showImportConfirm by remember { mutableStateOf(false) }
var logLineCount by remember { mutableIntStateOf(AppLog.count()) } var logLineCount by remember { mutableIntStateOf(AppLog.count()) }
// Message de feedback affiché en haut de l'écran (succès/échec des exports,
// imports, calibration T…) — déclaré AVANT les launchers : le callback de
// logExportLauncher l'écrit (un state local doit être déclaré avant le
// lambda qui le capture).
var message by remember { mutableStateOf<String?>(null) }
val logExportLauncher = rememberLauncherForActivityResult( val logExportLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.CreateDocument("text/plain") ActivityResultContracts.CreateDocument("text/plain")
) { uri -> ) { uri ->
// Même pattern que l'export JSON (BackupManager.writeBackup) : écriture // ── Export des LOGS — même PROCESSUS que l'export JSON (cf §14 #45) ──────
// synchronisée dans une coroutine IO, AppLog toutes les étapes // Historique : v1.3.1 plantait (écriture inline), le « fix » v1.3.2 a
// réécrit inline AUSSI (et laissait des AppLog.log hors try/catch) —
// l'app plantait toujours. Leçon §14 #41 : quand un pattern d'IO marche,
// le réutiliser TEL QUEL. L'export JSON (BackupManager.writeBackup)
// fonctionne sur le téléphone → l'export des logs passe par LA MÊME
// fonction (elle prend un String, peu importe son contenu).
//
// Points de robustesse :
// - le résultat SAF arrive sur le thread UI ; l'IO part dans une
// coroutine à SCOPE RACINE (CoroutineScope standalone, jamais
// rememberCoroutineScope : elle serait annulée si l'écran quittait la
// composition pendant l'écriture) ;
// - TOUT ce qui peut lever est sous try/catch : une exception non
// interceptée dans un scope racine remonte au handler de la thread
// = crash de l'app. (AppLog.log est de toute façon fail-safe depuis
// v1.3.3, double filet.)
uri ?: return@rememberLauncherForActivityResult
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
AppLog.log("logs", "export démarré") val ok = try {
try {
val logs = AppLog.all() val logs = AppLog.all()
val ok = uri != null && try { AppLog.log("logs", "export démarré (${logs.length} caractères)")
context.contentResolver.openOutputStream(uri, "wt")?.use { os -> // ⚠️ Code path IDENTIQUE à l'export JSON : BackupManager
os.write(logs.toByteArray(Charsets.UTF_8)) // .writeBackup (openOutputStream "wt", exceptions capturées).
os.flush() BackupManager.writeBackup(context, uri, logs)
} ?: false
true
} catch (e: Exception) { } catch (e: Exception) {
AppLog.log("logs", "ERREUR export : ${e.message}") AppLog.log("logs", "ERREUR export : ${e.message}")
false false
} }
AppLog.log("logs", "export terminé : ok=$ok, ${logs.length} caractères") AppLog.log("logs", "export terminé : ok=$ok")
} catch (e: Exception) { withContext(Dispatchers.Main) {
AppLog.log("logs", "ERREUR export (outer) : ${e.message}") // Feedback visible comme l'export JSON + compteur rafraîchi
// (l'export vient d'ajouter des lignes de journal). Écriture
// d'état Compose UNIQUEMENT sur le thread principal.
logLineCount = AppLog.count()
message = context.getString(
if (ok) R.string.logs_export_ok else R.string.logs_export_fail
)
} }
} }
} }
var importJson by remember { mutableStateOf<String?>(null) } var importJson by remember { mutableStateOf<String?>(null) }
var message by remember { mutableStateOf<String?>(null) }
val exportLauncher = rememberLauncherForActivityResult( val exportLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.CreateDocument("application/json") ActivityResultContracts.CreateDocument("application/json")

View File

@ -24,26 +24,50 @@ object AppLog {
private var file: File? = null private var file: File? = null
private val lock = Any() private val lock = Any()
/** À appeler une fois dans Application.onCreate. */ /**
* À appeler une fois dans Application.onCreate.
*
* ⚠️ La lecture du fichier existant est GARDÉE (try/catch) : un fichier de log
* corrompu ou illisible ne doit jamais empêcher l'app de démarrer — on perd
* l'historique, pas le process (même famille que le fix v1.3.3 de log()).
*/
fun init(context: android.content.Context) { fun init(context: android.content.Context) {
synchronized(lock) { synchronized(lock) {
val f = File(context.filesDir, "debug-log.txt") val f = File(context.filesDir, "debug-log.txt")
file = f file = f
if (f.exists()) { if (f.exists()) {
buffer.clear() buffer.clear()
buffer.addAll(f.readLines().takeLast(MAX_LINES)) val loaded = try {
f.readLines().takeLast(MAX_LINES)
} catch (_: Exception) {
emptyList() // fichier illisible → démarrer avec un buffer vide
}
buffer.addAll(loaded)
} }
} }
log("app", "démarrage (v init) — ${buffer.size} lignes de logs existantes") log("app", "démarrage (v init) — ${buffer.size} lignes de logs existantes")
} }
/**
* Journalise une ligne (buffer mémoire + append fichier).
*
* ⚠️ NE JAMAIS PROPAGER D'EXCEPTION (fix v1.3.3) : log() est appelé depuis
* des dizaines de sites, y compris dans des coroutines à scope racine où une
* exception non interceptée = crash du process (cf export des logs, §14 #45).
* L'IO fichier (appendText) peut lever IOException (disque plein, I/O error) :
* elle est avalée ici — on perd au pire UNE ligne de journal, jamais l'app.
*/
fun log(tag: String, message: String) = synchronized(lock) { fun log(tag: String, message: String) = synchronized(lock) {
val line = formatLine(System.currentTimeMillis(), tag, message) val line = formatLine(System.currentTimeMillis(), tag, message)
buffer.addLast(line) buffer.addLast(line)
val trimmed = trimLines(buffer.toList(), MAX_LINES) val trimmed = trimLines(buffer.toList(), MAX_LINES)
buffer.clear() buffer.clear()
buffer.addAll(trimmed) buffer.addAll(trimmed)
try {
file?.appendText(line + "\n") file?.appendText(line + "\n")
} catch (_: Exception) {
// Journalisation best-effort : ne jamais faire planter l'app pour un log
}
} }
fun all(): String = synchronized(lock) { buffer.joinToString("\n") } fun all(): String = synchronized(lock) { buffer.joinToString("\n") }

View File

@ -40,6 +40,8 @@
<string name="logs_count">%1$d lignes (rappels, agenda, import/export, erreurs)</string> <string name="logs_count">%1$d lignes (rappels, agenda, import/export, erreurs)</string>
<string name="logs_export">Exporter</string> <string name="logs_export">Exporter</string>
<string name="logs_clear">Effacer</string> <string name="logs_clear">Effacer</string>
<string name="logs_export_ok">Logs de diagnostic exportés</string>
<string name="logs_export_fail">Échec de l\'export des logs de diagnostic</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>

View File

@ -40,6 +40,8 @@
<string name="logs_count">%1$d lines (reminders, calendar, import/export, errors)</string> <string name="logs_count">%1$d lines (reminders, calendar, import/export, errors)</string>
<string name="logs_export">Export</string> <string name="logs_export">Export</string>
<string name="logs_clear">Clear</string> <string name="logs_clear">Clear</string>
<string name="logs_export_ok">Diagnostic logs exported</string>
<string name="logs_export_fail">Failed to export the diagnostic logs</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>

View File

@ -1,7 +1,41 @@
# 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.2`, …). Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.3.3`, …).
## [1.3.3] — 2026-09-06 (versionCode 17)
### Corrigé
- **Export des logs plantait TOUJOURS l'app** (remontée ; le « fix » v1.3.2
était insuffisant) : l'écriture était réimplémentée en inline au lieu de
réutiliser l'IO éprouvée de l'export JSON, et les appels `AppLog.log` du
callback étaient hors try/catch — une exception dans une coroutine à scope
racine (CoroutineScope standalone) remonte au handler de la thread =
crash du process. FIX : l'export passe désormais par
**`BackupManager.writeBackup` tel quel** (le même code path que l'export
JSON qui fonctionne sur le téléphone), tout le callback est gardé, et
`AppLog.log` ne lève plus jamais d'exception (IO fichier avalée en interne,
on perd au pire une ligne de journal). Feedback visible succès/échec
(comme l'export JSON) + compteur de lignes rafraîchi après l'export.
- **Événements d'agenda inopérants depuis v1.3.0** : les permissions
WRITE_CALENDAR / READ_CALENDAR n'étaient **pas déclarées dans le manifest**
(jamais présentes dans tout l'historique git, bien que documentées §9) —
la demande à l'exécution était donc refusée d'office et
`CalendarEvents` lève SecurityException. Cause racine manquante des bugs
« toggle agenda » « corrigés » en v1.3.1/v1.3.2 : la logique UI était
correcte, il ne venait à l'esprit de personne que la déclaration manquait.
- **Métadonnées de version fausses** : le bump de version n'avait jamais
été commité — les tags v1.3.0, v1.3.1 **et** v1.3.2 contenaient tous
`versionCode = 14 / versionName = "1.3.0"`, donc les APK publiés
affichaient « 1.3.0 » dans Paramètres et en titre du dialog « Nouveautés ».
Désormais bumpé + commité avant chaque tag (checklist §16 étape 1).
### Technique
- Doc de développement mise à jour (DB v3, targetSdk 36, fichiers de
données de régression, bugs #43–#45) ; téléphone de test documenté :
**Google Pixel 9 /e/OS** (AOSP, DocumentsUI standard — l'export JSON y
fonctionne, l'export logs réutilise ce même path).
- Compteur de tests inchangé : 87 (44 sans les données locales, gitignorées).
## [1.3.2] — 2026-09-06 (versionCode 16) ## [1.3.2] — 2026-09-06 (versionCode 16)

View File

@ -79,6 +79,7 @@ Fonctionnalités v1 :
| 6 sept. 2026 (session v1.3.1) | En-tête « temps sous THS » sur Doses ; **logs de diagnostic** (AppLog : buffer 500 lignes, persisté, exportable de Paramètres) ; **fix toggle agenda** (callback async) ; **87 tests verts**, APK v1.3.1 + releases. | | 6 sept. 2026 (session v1.3.1) | En-tête « temps sous THS » sur Doses ; **logs de diagnostic** (AppLog : buffer 500 lignes, persisté, exportable de Paramètres) ; **fix toggle agenda** (callback async) ; **87 tests verts**, APK v1.3.1 + releases. |
| 6 sept. 2026 (session v1.3.2) | 3 bugs remontés : toggle agenda (callback async fix + ContextCompat au save + Posologie requise), export logs plantait (pattern JSON réutilisé + AppLog), dialog changelog récurrent (version vue mémorisée avant affichage + titre BuildConfig documenté) ; export réel mis à jour (3 traitements : EV inactif + EEn actif + CPA oral) ; **87 tests verts**, APK v1.3.2 + releases. | | 6 sept. 2026 (session 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.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. |
@ -98,14 +99,14 @@ et en testant** — aucun build n'avait été lancé avant la session 3.
| Kotlin | 2.3.21 (plugin compose 2.3.21) | idem | | Kotlin | 2.3.21 (plugin compose 2.3.21) | idem |
| KSP | 2.3.11 (versionnage indépendant depuis KSP2) | idem | | KSP | 2.3.11 (versionnage indépendant depuis KSP2) | idem |
| Compose BOM | **2026.08.00** (Compose 1.12 ; material3 pinné par le BOM) | `app/build.gradle.kts` | | Compose BOM | **2026.08.00** (Compose 1.12 ; material3 pinné par le BOM) | `app/build.gradle.kts` |
| Room | 2.8.4 (KSP) — **DB v2 + MIGRATION_1_2**, `fallbackToDestructiveMigration` retiré | idem + `AppDatabase.kt` | | Room | 2.8.4 (KSP) — **DB v3 + MIGRATION_1_2 (forecastIntervalDays) + MIGRATION_2_3 (calendarEventId)**, `fallbackToDestructiveMigration` retiré | idem + `AppDatabase.kt` |
| Navigation Compose | 2.10.0 | idem | | Navigation Compose | 2.10.0 | idem |
| AppCompat | 1.8.0 (langue par app) | idem | | AppCompat | 1.8.0 (langue par app) | idem |
| DataStore Preferences | 1.2.1 | idem | | DataStore Preferences | 1.2.1 | idem |
| Gson | 2.14.0 | idem | | Gson | 2.14.0 | idem |
| JUnit | 4.13.2 (testImplementation) | idem | | JUnit | 4.13.2 (testImplementation) | idem |
| WorkManager | 2.11.2 (déclaré, non utilisé — supprimable) | idem | | WorkManager | 2.11.2 (déclaré, non utilisé — supprimable) | idem |
| compileSdk / targetSdk | **37** / 37 ; minSdk 26 ; Java target 17 | app | | compileSdk / targetSdk | **37** / **36** ; minSdk 26 ; Java target 17 | app |
| buildFeatures | compose + **buildConfig** (VERSION_NAME pour l'app) | app | | buildFeatures | compose + **buildConfig** (VERSION_NAME pour l'app) | app |
Notes importantes (v1.2.0) : Notes importantes (v1.2.0) :
@ -129,6 +130,12 @@ Notes importantes (v1.2.0) :
## 4. Environnement de build (cette machine) ## 4. Environnement de build (cette machine)
- **macOS (Apple Silicon), brew présent, Java 21 (Microsoft OpenJDK) sur `/usr/bin/java`** ✓ - **macOS (Apple Silicon), brew présent, Java 21 (Microsoft OpenJDK) sur `/usr/bin/java`** ✓
- **Téléphone de test** : **Google Pixel 9 sous /e/OS** (ROM dé-Googlée, base AOSP) —
SAF/DocumentsUI standard (l'export JSON via `CreateDocument` y fonctionne → c'est LE
pattern d'IO de référence pour tout export de fichier) ; pas de Play Store, sideload
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,
cf §17 — ce n'est PAS le téléphone.)
- **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`
@ -166,9 +173,12 @@ Points clés :
## 6. Modèle de données (Room) ## 6. Modèle de données (Room)
DB `hormonetrack.db`, **version 2**, migrations explicites (⚠️ plus de DB `hormonetrack.db`, **version 3**, migrations explicites (⚠️ plus de
`fallbackToDestructiveMigration` — retiré en v1.2.0 car l'utilisatrice a des données `fallbackToDestructiveMigration` — retiré en v1.2.0 car l'utilisatrice a des données
réelles ; toute évolution de schéma = `Migration(x, y)` + ALTER TABLE). réelles ; toute évolution de schéma = `Migration(x, y)` + ALTER TABLE) :
- **v1 → v2** : `forecastIntervalDays REAL` (prévision, v1.2.0)
- **v2 → v3** : `calendarEventId INTEGER` (agenda récurrent, v1.3.0)
### `Treatment` (treatments) ### `Treatment` (treatments)
- base : `id`, `name`, `type` (ESTRADIOL/ANTI_ANDROGEN/PROGESTOGEN/OTHER), `route` - base : `id`, `name`, `type` (ESTRADIOL/ANTI_ANDROGEN/PROGESTOGEN/OTHER), `route`
@ -389,12 +399,14 @@ Emplacement : `app/src/test/java/com/hormonetrack/`. Répertoire de travail d'ex
non calibré ≈ 367 = les « 375 » rapportés), facteur unique EEN plausible, lab T en non calibré ≈ 367 = les « 375 » rapportés), facteur unique EEN plausible, lab T en
unité aberrante neutralisé, prévision 6 j exacte, auto-cal cohérente. unité aberrante neutralisé, prévision 6 j exacte, auto-cal cohérente.
**Tout nouvel export utilisateur = un nouveau test de régression.** **Tout nouvel export utilisateur = un nouveau test de régression.**
- **`RegressionUserCase3Test`** (6) : 3ᵉ régression (export v1.2.3) — le scénario - **`RegressionUserCase3Test`** (6) : 3ᵉ régression (export **v1.3.1** — mis à
**transition** : traitement **EV inactif** (29 doses 2–8 mg, janvier→juillet) + jour à la v1.3.2) — le scénario **transition** : traitement **EV inactif**
traitement **EEn actif** (9 doses), 22 labs sur les deux périodes. Vérifie : (29 doses 2–8 mg, janvier→juillet) + traitement **EEn actif** (9 doses) +
parsing, **l'inactif reste simulé** (bug v1.2.4 — le moteur reçoit TOUS les **CPA oral** (anti-androgène), soit 3 traitements, 51 doses, 26 labs.
traitements), calibration par période couvrant EV **et** EEN, k T par ester, Vérifie : parsing, **l'inactif reste simulé** (bug v1.2.4 — le moteur reçoit
continuité de la courbe pendant la transition, niveau actuel ~EEn équilibre. TOUS les traitements), calibration par période couvrant EV **et** EEN,
k T par ester, continuité de la courbe pendant la transition, niveau
actuel ~EEn équilibre.
### 8.bis Données de test réelles : HORS dépôt (`local-test-data/`) ### 8.bis Données de test réelles : HORS dépôt (`local-test-data/`)
@ -403,8 +415,11 @@ du moteur sur les **exports réels** de l'utilisatrice. Ce sont des **données d
personnelles** : elles ne sont **pas versionnées**, pour ne rien divulguer dans le personnelles** : elles ne sont **pas versionnées**, pour ne rien divulguer dans le
dépôt (ni maintenant, ni si le repo devient public un jour). dépôt (ni maintenant, ni si le repo devient public un jour).
- emplacement : `local-test-data/backup-v1.0.0.json` et `backup-v1.2.0.json` - emplacement : `local-test-data/backup-v1.0.0.json`, `backup-v1.2.0.json`,
(copiés tels quels depuis l'export JSON de l'app) ; `backup-v1.2.3.json` (ancien export, plus consommé par un test) et
`backup-v1.3.1.json` (copiés tels quels depuis l'export JSON de l'app) ;
`RegressionUserCaseTest` lit v1.0.0, `RegressionUserCase2Test` lit v1.2.0,
`RegressionUserCase3Test` lit **v1.3.1** ;
- `.gitignore` contient `local-test-data/` → jamais commités ; - `.gitignore` contient `local-test-data/` → jamais commités ;
- les tests font `Assume.assumeTrue(file.exists())` dans le `@Before` : **sans le - les tests font `Assume.assumeTrue(file.exists())` dans le `@Before` : **sans le
fichier, la classe est IGNORÉE** (skipped, pas failed) — un clone neuf ou une CI fichier, la classe est IGNORÉE** (skipped, pas failed) — un clone neuf ou une CI
@ -474,7 +489,12 @@ l'entier PAIR !), début = prochaine occurrence à l'heure de rappel (ou 12:00).
L'id est stocké sur le traitement (`calendarEventId`, Room v3) ; au save : L'id est stocké sur le traitement (`calendarEventId`, Room v3) ; au save :
supprimer + recréer (fiable) si changé, supprimer si désactivé. Permissions supprimer + recréer (fiable) si changé, supprimer si désactivé. Permissions
WRITE_CALENDAR + READ_CALENDAR demandées à l'activation du switch. WRITE_CALENDAR + READ_CALENDAR demandées à l'activation du switch.
Manifest : `POST_NOTIFICATIONS`, `SCHEDULE_EXACT_ALARM`, `RECEIVE_BOOT_COMPLETED`, `VIBRATE`, `WRITE_CALENDAR`, `READ_CALENDAR`. Manifest : `POST_NOTIFICATIONS`, `SCHEDULE_EXACT_ALARM`, `RECEIVE_BOOT_COMPLETED`,
`VIBRATE`, `WRITE_CALENDAR`, `READ_CALENDAR`.
⚠️ **Permissions agenda (v1.3.3)** : WRITE_CALENDAR + READ_CALENDAR ont été
ajoutées au manifest seulement en v1.3.3 — elles étaient documentées ici
depuis v1.3.0 mais **jamais déclarées dans le XML** (bug #44, §14) : tout
jeton « le manifest contient X » doit être VÉRIFIÉ dans le fichier réel.
Sur la montre : remontée par Gadgetbridge **ou** Huawei Health (cf §17). Sur la montre : remontée par Gadgetbridge **ou** Huawei Health (cf §17).
## 10. UI & navigation ## 10. UI & navigation
@ -735,12 +755,18 @@ Historique complet — **à ne pas réintroduire** (utile pour diff/revert) :
(openOutputStream "wt") + journalisation AppLog à chaque étape. (openOutputStream "wt") + journalisation AppLog à chaque étape.
Leçon : quand un pattern d'IO marche (export JSON), le réutiliser Leçon : quand un pattern d'IO marche (export JSON), le réutiliser
tel quel plutôt que d'en écrire un nouveau. tel quel plutôt que d'en écrire un nouveau.
41. **Comparaison lexicographique de tags/versions** (v1.3.0, publication 38. **Piège 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. →
toujours comparer les versions STRUCTURÉEMENT (tuple numérique, cf toujours comparer les versions STRUCTURÉEMENT (tuple numérique, cf
ChangelogHelper.isVersionNewer) ; même famille que le bug #22 (casse ChangelogHelper.isVersionNewer) ; même famille que le bug #22 (casse
EEn) : identifiants « presque pareils ». EEn) : identifiants « presque pareils ». (Numéro corrigé en v1.3.3 :
l'entrée était dupliquée sous « #41 » alors que §2/§16 référencent #38.)
43. **Course entre uploads rapprochés** (v1.2.6→v1.3.2, confirmé 4 fois) :
publier les 2 APK par DEUX INVOCATIONS rapprochées de gitea-release.py
→ le second upload écrase le premier (APK release disparu de la
release). → `scripts/publish-release.py` : UNE invocation fait purge +
upload des 2 APK + vérification PAR TÉLÉCHARGEMENT de chacun (§16.bis).
36. **Sens des boutons de zoom inversé** (v1.2.10, retour utilisateur) : 36. **Sens des boutons de zoom inversé** (v1.2.10, retour utilisateur) :
le « + » dézoomait (fenêtre plus longue) et le « − » zoomait — contre le « + » dézoomait (fenêtre plus longue) et le « − » zoomait — contre
la convention carte. → « + » = zoom avant (fenêtre courte), « − » = la convention carte. → « + » = zoom avant (fenêtre courte), « − » =
@ -764,6 +790,41 @@ Historique complet — **à ne pas réintroduire** (utile pour diff/revert) :
(chips, dropdown création, rappels) ; simulation et calibration reçoivent (chips, dropdown création, rappels) ; simulation et calibration reçoivent
TOUS les traitements. Épinglé par la régression n°3. TOUS les traitements. Épinglé par la régression n°3.
**Session v1.3.3 (audit de reprise de maintenance — session IA démarrant sans
aucun contexte, dans l'esprit de cette doc) :**
44. **Permissions agenda absentes du manifest** (v1.3.0 → v1.3.2) : la
fonctionnalité « événement d'agenda récurrent » ne pouvait JAMAIS
fonctionner — `WRITE_CALENDAR`/`READ_CALENDAR` n'étaient **pas déclarées
dans AndroidManifest.xml** (la demande limitée à l'exécution est refusée
d'office sans déclaration, et `CalendarEvents.ensureCalendar/upsert`
lèvent SecurityException). La doc §9 les listait pourtant depuis v1.3.0
→ leçon : « documenté » ≠ « implémenté » ; toute affirmation « le
manifest contient X » doit être vérifiée dans le XML réel (30 s de
`grep uses-permission`). Les fixes v1.3.1/v1.3.2 du « toggle agenda »
réparaient la logique UI mais pas la cause racine. FIX : déclaration +
commentaire dans le manifest.
45. **Export des logs plantait TOUJOURS l'app** (le « fix » v1.3.2, #41
plus haut, était insuffisant) : l'IO restait réimplémentée inline
(leçon du #41 : réutiliser BackupManager — PAS appliquée) et 3 appels
`AppLog.log` du callback étaient HORS try/catch — or une exception non
interceptée dans une coroutine à scope racine (`CoroutineScope
(Dispatchers.IO)`) remonte au handler de la thread = crash du process.
FIX : (a) l'export réutilise `BackupManager.writeBackup` TEL QUEL —
le même code path que l'export JSON qui marche sur le Pixel 9 /e/OS ;
(b) tout le callback est gardé ; (c) `AppLog.log` est fail-safe
(l'IO fichier est avalée dans le wrapper — une erreur disque ne doit
jamais tuer l'app pour une ligne de journal) ; (d) feedback visible
succès/échec + `logLineCount` rafraîchi (sur Main).
46. **Bump de version jamais commité** (v1.3.0 → v1.3.2) : les tags
v1.3.0/v1.3.1/v1.3.2 contenaient TOUS `versionCode = 14`
/ `versionName = "1.3.0"` → `BuildConfig.VERSION_NAME` était faux dans
les APK publiés (Paramètres, titre du dialog « Nouveautés »,
comparaison `isVersionNewer`). La checklist §16 étape 1 existait mais
n'a jamais été appliquée. FIX : versionCode 17 / « 1.3.3 » commité
AVANT le tag ; le bump fait partie du commit de release (jamais un
état local non commité au moment du build).
**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
@ -773,13 +834,12 @@ synthétiques ratent — mais garde ces données **hors du dépôt** (§8.bis) ;
(e) attention aux identifiants « presque pareils » entre sources (constantes app (e) attention aux identifiants « presque pareils » entre sources (constantes app
vs clés d'asset) ; (f) après un échec de COMPILATION, jeter les résultats de tests vs clés d'asset) ; (f) après un échec de COMPILATION, jeter les résultats de tests
de la même passe (XML périmés) ; (g) tout script de réécriture d'historique doit de la même passe (XML périmés) ; (g) tout script de réécriture d'historique doit
être idempotent ; (h) dans un KDoc, `/**` imbrique. (a) ne jamais croire un build « probablement bon » sans l'avoir lancé ; être idempotent ; (h) dans un KDoc, `/**` imbrique ; (i) quand un pattern d'IO
(b) les tests sémantiques attrapent ce que la compilation ne voit pas ; (c) se méfier des marche (export JSON), le réutiliser TEL QUEL — une réimplémentation « équivalente »
constantes stdlib « de mémoire » (`ln2`), des mélanges Float/Double, et des APIs M3 perd les garde-fous acquis à l'usage (cf #45) ; (j) vérifier les POSTULATS dans le
expérimentales sans `@OptIn` ; (d) **un test de régression sur les VRAIES données code réel, pas dans la doc (« le manifest contient WRITE_CALENDAR », cf #44) ;
utilisateur** (`RegressionUserCaseTest` = export JSON réel) attrape les bugs de (k) un bump de version non commité = métadonnées fausses dans les APK publiés
convention (casse, unités) que les tests synthétiques ratent ; (e) attention aux (cf #46) — le bump fait partie du commit de release.
identifiants « presque pareils » entre sources (constantes app vs clés d'asset).
## 15. Comment régénérer l'asset pk_profiles.json ## 15. Comment régénérer l'asset pk_profiles.json
@ -975,7 +1035,6 @@ Volontaires (v1) :
- Pas de ViewModel/DI (couplage UI↔repo via CompositionLocal) - Pas de ViewModel/DI (couplage UI↔repo via CompositionLocal)
- Modèle T empirique (non publié) — étiqueté estimation partout - Modèle T empirique (non publié) — étiqueté estimation partout
- Import JSON = **écrasement** depuis v1.2.6 (mode fusion non implémenté) - Import JSON = **écrasement** depuis v1.2.6 (mode fusion non implémenté)
- `fallbackToDestructiveMigration()` — à retirer à la migration v2 du schéma
- WorkManager déclaré non utilisé - WorkManager déclaré non utilisé
- Profils par **tables** (pas par formule) : les D/k1–k3 de l'ODS ne sont pas consommés — - Profils par **tables** (pas par formule) : les D/k1–k3 de l'ODS ne sont pas consommés —
rétro-ingénierie des fits non tentée ; les tables sont exactes rétro-ingénierie des fits non tentée ; les tables sont exactes
@ -995,16 +1054,17 @@ Volontaires (v1) :
3. Mode « planifier les injections » (schedule récurrent → pré-remplir le log) 3. Mode « planifier les injections » (schedule récurrent → pré-remplir le log)
4. Import JSON : détection de doublons / mode fusion optionnel (l'écrasement est fait, v1.2.6) 4. Import JSON : détection de doublons / mode fusion optionnel (l'écrasement est fait, v1.2.6)
5. Verrou biométrique (BiometricPrompt), widget, export CSV 5. Verrou biométrique (BiometricPrompt), widget, export CSV
6. Charts : **zoom** + tooltip au toucher (le pan est fait, v1.2.0) ; 6. Charts : tooltip au toucher (le pan est fait v1.2.0, **le zoom v1.2.9**) ;
MaterialExpressiveTheme quand l'API passera publique (cf §3) MaterialExpressiveTheme quand l'API passera publique (cf §3)
7. **Vrai keystore de distribution** (signature dédiée ≠ clé debug → nécessite 7. **Vrai keystore de distribution** (signature dédiée ≠ clé debug → nécessite
une migration : backup → désinstallation → installation signée → réimport) une migration : backup → désinstallation → installation signée → réimport)
Fait (à ne pas refaire) : pan du chart (v1.2.0), pics/creux (v1.2.3), Fait (à ne pas refaire) : pan du chart (v1.2.0), **zoom du chart (v1.2.9)**,
prévision par fréquence (v1.2.0), calibration par période d'ester E2 **et** T pics/creux (v1.2.3), prévision par fréquence (v1.2.0), calibration par période
(v1.2.1/v1.2.3), édition doses (v1.1.0) et labs (v1.2.2), E2+T en une entrée d'ester E2 **et** T (v1.2.1/v1.2.3), édition doses (v1.1.0) et labs (v1.2.2),
(v1.2.2), migration Room v1→v2 sans fallback destructif (v1.2.0), dépôt Gitea E2+T en une entrée (v1.2.2), migration Room v1→v2 sans fallback destructif
+ releases APK (push session). (v1.2.0), rooms v2→v3 (calendarEventId, v1.3.0), dépôt Gitea + releases APK
(push session).
8. Phase 2 montre : watchface `.hwt` custom, puis mini-app Lite Wearable (cf §17) 8. Phase 2 montre : watchface `.hwt` custom, puis mini-app Lite Wearable (cf §17)
9. Retirer WorkManager ou l'utiliser (reschedule de sécurité quotidien) 9. Retirer WorkManager ou l'utiliser (reschedule de sécurité quotidien)
@ -1045,6 +1105,25 @@ Sur le téléphone de test (à compléter par l'utilisatrice) :
- [ ] **v1.2.4** : passer un traitement à inactif → retiré de « Log rapide » et - [ ] **v1.2.4** : passer un traitement à inactif → retiré de « Log rapide » et
du dropdown des nouvelles doses, rappel annulé, **mais sa simulation reste du dropdown des nouvelles doses, rappel annulé, **mais sa simulation reste
sur le graphique** et la calibration couvre toujours ses périodes sur le graphique** et la calibration couvre toujours ses périodes
- [ ] **v1.3.3** : **export des logs** → le gestionnaire de fichiers s'ouvre,
le .txt est écrit, un message « Logs de diagnostic exportés » s'affiche ;
cas d'échec (annulation du chooser = silencieux, échec d'écriture =
message d'erreur, l'app ne doit JAMAIS planter — bug #45, Pixel 9 /e/OS)
- [ ] **v1.3.3** : **version affichée** « 1.3.3 » dans Paramètres et en titre du
dialog « Nouveautés » (bug #46 : les APK v1.3.0–1.3.2 affichaient 1.3.0) ;
au premier lancement de v1.3.3, le dialog liste 1.3.1→1.3.3 (dernière vue
mémorisée = « 1.3.0 » sur les anciens APK)
- [ ] **v1.3.3** : **événement d'agenda** : le switch demande la permission
agenda (cette fois le système doit vraiment montrer le dialogue de
permission — elle est désormais dans le manifest, bug #44) ; après
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
de backup + graphiques (réflexion Gson sous R8)
--- ---
*Doc mise à jour le 6 sept. 2026 (v1.3.2) — build OK, 87/87 tests verts (44 sans les données locales), dépôts Gitea (cloudyfy + farewell) avec releases APK, aucune donnée de santé dans le dépôt ni l'historique.* *Doc mise à jour le 6 sept. 2026 (v1.3.3) — build OK, 87/87 tests verts (44 sans les
données locales), dépôts Gitea (cloudyfy + farewell) avec releases APK, aucune donnée
de santé dans le dépôt ni l'historique. v1.3.3 = session de reprise de maintenance :
3 bugs racines corrigés (permissions agenda, export logs, bump de version) ; doc
raccourcie (DB v3, targetSdk 36, régression 3 = export v1.3.1, numéros #38/#43 rétablis,
staleness §19/§20 nettoyé).*