diff --git a/README.md b/README.md index b1049a8..8f4c1fc 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ sauvegarde JSON. **100 % local, aucun compte, aucun serveur.** > à titre informatif — ce ne sont pas des mesures. Fie-toi toujours à tes analyses de > sang et aux consignes de ton endocrinologue. -- **Statut** : v1.3.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) - **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) @@ -73,7 +73,7 @@ par AGP si les licences sont signées). Le wrapper télécharge Gradle 9.7.1. git clone && cd HormoneTrack echo "sdk.dir=/chemin/vers/android-sdk" > local.properties # ou ANDROID_HOME ./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 @@ -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) — `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)) : `-release.apk` (**recommandé**, optimisé R8, 2,4 Mo) et `-debug.apk` (20 Mo) : diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bd52e87..ac83add 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,8 +12,13 @@ android { applicationId = "com.hormonetrack" minSdk = 26 targetSdk = 36 - versionCode = 14 - versionName = "1.3.0" + // ⚠️ MÉTADONNÉES DE VERSION — bump obligatoire à CHAQUE release + // (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" vectorDrawables { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ce81ee2..5acbbbb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,15 @@ + + + + Unit) { var showImportConfirm by remember { mutableStateOf(false) } 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(null) } val logExportLauncher = rememberLauncherForActivityResult( ActivityResultContracts.CreateDocument("text/plain") ) { uri -> - // Même pattern que l'export JSON (BackupManager.writeBackup) : écriture - // synchronisée dans une coroutine IO, AppLog toutes les étapes + // ── Export des LOGS — même PROCESSUS que l'export JSON (cf §14 #45) ────── + // 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 { - AppLog.log("logs", "export démarré") - try { + val ok = try { val logs = AppLog.all() - val ok = uri != null && try { - context.contentResolver.openOutputStream(uri, "wt")?.use { os -> - os.write(logs.toByteArray(Charsets.UTF_8)) - os.flush() - } ?: false - true - } catch (e: Exception) { - AppLog.log("logs", "ERREUR export : ${e.message}") - false - } - AppLog.log("logs", "export terminé : ok=$ok, ${logs.length} caractères") + AppLog.log("logs", "export démarré (${logs.length} caractères)") + // ⚠️ Code path IDENTIQUE à l'export JSON : BackupManager + // .writeBackup (openOutputStream "wt", exceptions capturées). + BackupManager.writeBackup(context, uri, logs) } catch (e: Exception) { - AppLog.log("logs", "ERREUR export (outer) : ${e.message}") + AppLog.log("logs", "ERREUR export : ${e.message}") + false + } + AppLog.log("logs", "export terminé : ok=$ok") + withContext(Dispatchers.Main) { + // 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(null) } - var message by remember { mutableStateOf(null) } val exportLauncher = rememberLauncherForActivityResult( ActivityResultContracts.CreateDocument("application/json") diff --git a/app/src/main/java/com/hormonetrack/util/AppLog.kt b/app/src/main/java/com/hormonetrack/util/AppLog.kt index fc883e9..934bddd 100644 --- a/app/src/main/java/com/hormonetrack/util/AppLog.kt +++ b/app/src/main/java/com/hormonetrack/util/AppLog.kt @@ -24,26 +24,50 @@ object AppLog { private var file: File? = null 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) { synchronized(lock) { val f = File(context.filesDir, "debug-log.txt") file = f if (f.exists()) { 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") } + /** + * 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) { 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") + try { + 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") } diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 7edc268..67a12c1 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -40,6 +40,8 @@ %1$d lignes (rappels, agenda, import/export, erreurs) Exporter Effacer + Logs de diagnostic exportés + Échec de l\'export des logs de diagnostic Nouveautés de la version %1$s Version installée : %1$s Dernières versions sur Gitea (télécharger les APK) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 255c432..dd570b5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -40,6 +40,8 @@ %1$d lines (reminders, calendar, import/export, errors) Export Clear + Diagnostic logs exported + Failed to export the diagnostic logs What\'s new in %1$s Installed version: %1$s Latest releases on Gitea (download APKs) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 514054b..f5f2bbc 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,7 +1,41 @@ # Changelog — HormoneTrack Format : [Keep a Changelog](https://keepachangelog.com/fr-FR/1.1.0/). -Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.3.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) diff --git a/docs/DEVELOPPEMENT.md b/docs/DEVELOPPEMENT.md index b891b01..6fa1987 100644 --- a/docs/DEVELOPPEMENT.md +++ b/docs/DEVELOPPEMENT.md @@ -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.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 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. | | 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. | @@ -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 | | 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` | -| 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 | | AppCompat | 1.8.0 (langue par app) | idem | | DataStore Preferences | 1.2.1 | idem | | Gson | 2.14.0 | idem | | JUnit | 4.13.2 (testImplementation) | 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 | Notes importantes (v1.2.0) : @@ -129,6 +130,12 @@ Notes importantes (v1.2.0) : ## 4. Environnement de build (cette machine) - **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` → `/opt/homebrew/share/android-commandlinetools` (524 MB) - licences acceptées : `yes | sdkmanager --licenses` @@ -166,9 +173,12 @@ Points clés : ## 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 -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) - 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 unité aberrante neutralisé, prévision 6 j exacte, auto-cal cohérente. **Tout nouvel export utilisateur = un nouveau test de régression.** -- **`RegressionUserCase3Test`** (6) : 3ᵉ régression (export v1.2.3) — le scénario - **transition** : traitement **EV inactif** (29 doses 2–8 mg, janvier→juillet) + - traitement **EEn actif** (9 doses), 22 labs sur les deux périodes. Vérifie : - parsing, **l'inactif reste simulé** (bug v1.2.4 — le moteur reçoit 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. +- **`RegressionUserCase3Test`** (6) : 3ᵉ régression (export **v1.3.1** — mis à + jour à la v1.3.2) — le scénario **transition** : traitement **EV inactif** + (29 doses 2–8 mg, janvier→juillet) + traitement **EEn actif** (9 doses) + + **CPA oral** (anti-androgène), soit 3 traitements, 51 doses, 26 labs. + Vérifie : parsing, **l'inactif reste simulé** (bug v1.2.4 — le moteur reçoit + 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/`) @@ -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 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` - (copiés tels quels depuis l'export JSON de l'app) ; +- emplacement : `local-test-data/backup-v1.0.0.json`, `backup-v1.2.0.json`, + `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 ; - 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 @@ -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 : supprimer + recréer (fiable) si changé, supprimer si désactivé. Permissions 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). ## 10. UI & navigation @@ -735,12 +755,18 @@ Historique complet — **à ne pas réintroduire** (utile pour diff/revert) : (openOutputStream "wt") + journalisation AppLog à chaque étape. Leçon : quand un pattern d'IO marche (export JSON), le réutiliser 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 < v1.2.5` ("1" < "5") → la boucle de publication a sauté v1.2.10. → toujours comparer les versions STRUCTURÉEMENT (tuple numérique, cf 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) : le « + » dézoomait (fenêtre plus longue) et le « − » zoomait — contre 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 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é ; (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 @@ -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 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 -être idempotent ; (h) dans un KDoc, `/**` imbrique. (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 -constantes stdlib « de mémoire » (`ln2`), des mélanges Float/Double, et des APIs M3 -expérimentales sans `@OptIn` ; (d) **un test de régression sur les VRAIES données -utilisateur** (`RegressionUserCaseTest` = export JSON réel) attrape les bugs de -convention (casse, unités) que les tests synthétiques ratent ; (e) attention aux -identifiants « presque pareils » entre sources (constantes app vs clés d'asset). +être idempotent ; (h) dans un KDoc, `/**` imbrique ; (i) quand un pattern d'IO +marche (export JSON), le réutiliser TEL QUEL — une réimplémentation « équivalente » +perd les garde-fous acquis à l'usage (cf #45) ; (j) vérifier les POSTULATS dans le +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 +(cf #46) — le bump fait partie du commit de release. ## 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) - Modèle T empirique (non publié) — étiqueté estimation partout - 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é - 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 @@ -995,16 +1054,17 @@ Volontaires (v1) : 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) 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) 7. **Vrai keystore de distribution** (signature dédiée ≠ clé debug → nécessite 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), -prévision par fréquence (v1.2.0), calibration par période d'ester E2 **et** T -(v1.2.1/v1.2.3), édition doses (v1.1.0) et labs (v1.2.2), E2+T en une entrée -(v1.2.2), migration Room v1→v2 sans fallback destructif (v1.2.0), dépôt Gitea -+ releases APK (push session). +Fait (à ne pas refaire) : pan du chart (v1.2.0), **zoom du chart (v1.2.9)**, +pics/creux (v1.2.3), prévision par fréquence (v1.2.0), calibration par période +d'ester E2 **et** T (v1.2.1/v1.2.3), édition doses (v1.1.0) et labs (v1.2.2), +E2+T en une entrée (v1.2.2), migration Room v1→v2 sans fallback destructif +(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) 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 du dropdown des nouvelles doses, rappel annulé, **mais sa simulation reste 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é).*