Compare commits

..

2 Commits

9 changed files with 252 additions and 58 deletions

View File

@ -16,7 +16,7 @@ sauvegarde JSON. **100 % local, aucun compte, aucun serveur.**
> à titre informatif — ce ne sont pas des mesures. Fie-toi toujours à tes analyses de
> sang et aux consignes de ton endocrinologue.
- **Statut** : v1.3.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 <repo> && 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) :

View File

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

View File

@ -6,6 +6,15 @@
<uses-permission android:name="android.permission.VIBRATE" />
<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
android:name=".HormoneTrackApp"
android:allowBackup="false"

View File

@ -78,33 +78,56 @@ fun SettingsScreen(onBack: () -> 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<String?>(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
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 : ${e.message}")
false
}
AppLog.log("logs", "export terminé : ok=$ok, ${logs.length} caractères")
} catch (e: Exception) {
AppLog.log("logs", "ERREUR export (outer) : ${e.message}")
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<String?>(null) }
var message by remember { mutableStateOf<String?>(null) }
val exportLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.CreateDocument("application/json")

View File

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

View File

@ -40,6 +40,8 @@
<string name="logs_count">%1$d lignes (rappels, agenda, import/export, erreurs)</string>
<string name="logs_export">Exporter</string>
<string name="logs_clear">Effacer</string>
<string name="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="settings_version">Version installée : %1$s</string>
<string name="releases_link">Dernières versions sur Gitea (télécharger les APK)</string>

View File

@ -40,6 +40,8 @@
<string name="logs_count">%1$d lines (reminders, calendar, import/export, errors)</string>
<string name="logs_export">Export</string>
<string name="logs_clear">Clear</string>
<string name="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="settings_version">Installed version: %1$s</string>
<string name="releases_link">Latest releases on Gitea (download APKs)</string>

View File

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

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.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
@ -418,8 +433,20 @@ dépôt (ni maintenant, ni si le repo devient public un jour).
- **l'historique a été nettoyé avant le premier push** : les premiers commits
embarquaient les valeurs (tests + doc) → `git filter-branch --tree-filter` avec
un script d'anonymisation (timestamps décalés de +30 j, valeurs perturbées dans
la prose des docs), tags réécrits, refs purgeées. `git grep` sur **toutes** les
révisions ne trouve aucune donnée réelle.
la prose des docs), tags réécrits, refs purgeées.
⚠️ **Lint de dette connue (constat v1.3.3)** : le scanner travaillait sur une liste
de motifs figée ; depuis que `local-test-data/` s'enrichit de NOUVEAUX exports
(ex. backup-v1.3.1.json ajouté à la v1.3.2), la ré-extraction des valeurs fait
que le scan `--history` détecte des fragments JSON de labs **encore présents
dans l'historique des révisions v1.1.0 → v1.2.3** (tests `RegressionUserCase{,2}`
data-driven AVANT la migration fichiers gitignorés ; fragments de la forme
« value » + timestamp). Amplitude : quelques valeurs numériques de labs — les
mêmes ordres de grandeur figurent déjà en PROSE dans les docs publiés (§14 #23,
CHANGELOG 1.1.0). Le working tree est propre. **Décision utilisateur (6 sept.
2026)** : purge complète de l'historique NON relancée (les révisions sont déjà
répliquées sur les deux Gitea, aucune fuite nouvelle par push) ; la dette
(réécriture d'historique + re-tags + force-push des 2 remotes + re-publish des
releases) reste ouverte et doit être re-décisionnée si le dépôt passe PUBLIC.
- **`LabsGroupingTest`** (4) : v1.2.2 — regroupement de l'écran Analyses (paire E2+T
même timestamp ; timestamps différents séparés ; tri E2 avant T avant autres ;
ordre chronologique décroissant).
@ -474,7 +501,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 +767,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 +802,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 +846,16 @@ 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 ; (l) le scan de
confidentialité s'ADAPTE aux nouveaux exports (`local-test-data/`) : se régénère
donc à CHECK chaque push — un historique « propre hier » peut devenir hit dès
qu'un nouvel export introduit des motifs qui collent ; juger les hits avec
l'utilisatrice (cf dette documentée §8.bis).
## 15. Comment régénérer l'asset pk_profiles.json
@ -975,7 +1051,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 +1070,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 +1121,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é).*