v1.3.5 : agenda réparé de bout en bout (remontée par les logs AppLog exportés) — #50 ensureCalendar : CALLER_IS_SYNCADAPTER exige account+account_type ; #50bis deleteEvent : même défaut, 'supprimé' logué mais event restant ; #51 buildTreatment perdait calendarEventId → événements orphelins ; log agenda véridique (3 messages) ; cycle ON→save→OFF→save validé émulateur ; 90 tests verts + lint

This commit is contained in:
Siphonight 2026-09-06 14:46:50 +02:00
parent 71659b5647
commit ef0fdbb01b
7 changed files with 155 additions and 17 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.4 — build Android ✅, **lint vert** ✅, **90 tests unitaires** ✅ (72 sans les données de test locales ; 3 régressions épinglées sur données réelles **non versionnées**), intégration montre = notifications ✅, **dépôt Gitea privé + releases avec APK** ✅
- **Statut** : v1.3.5 — build Android ✅, **lint vert** ✅, **90 tests unitaires** ✅ (72 sans les données de test locales ; 3 régressions épinglées sur données réelles **non versionnées**), intégration montre = notifications ✅, **dépôt Gitea privé + releases avec APK** ✅
- **Journal des versions** : [docs/CHANGELOG.md](docs/CHANGELOG.md)
- **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)
@ -85,7 +85,7 @@ Pas de Play Store : l'app est sideloadée. Détails pas-à-pas : [docs/GUIDE_INS
Dépôts : **gitea.cloudyfy.fr** et **gitea.farewell.dev** (miroir) —
`Siphonight/HormoneTrack` sur les deux (privé), avec
**releases taguées** (`v1.1.0` → `v1.3.4`) et **deux APK par release** (depuis v1.2.5)
**releases taguées** (`v1.1.0` → `v1.3.5`) 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

@ -17,8 +17,8 @@ android {
// 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 = 18
versionName = "1.3.4"
versionCode = 19
versionName = "1.3.5"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

View File

@ -64,11 +64,25 @@ object CalendarEvents {
* Retourne l'id du calendrier local « HormoneTrack » (le crée s'il manque).
* Nécessite WRITE_CALENDAR (l'insertion d'un calendrier local se fait avec
* CALLER_IS_SYNCADAPTER=true).
*
* ⚠️ BUG fixé (v1.3.5, #50) : un URI avec CALLER_IS_SYNCADAPTER DOIT aussi
* embarquer ACCOUNT_NAME + ACCOUNT_TYPE — sinon le CalendarProvider lève
* `IllegalArgumentException: Sync adapters must specify an account and
* account type` sur la requête ou l'insertion. Vu dans les AppLog exportés
* du Pixel 9 /e/OS v1.3.4 : la permission agenda était accordée (fix
* manifest v1.3.3 ✓) et ensureCalendar plantait juste après.
*/
fun ensureCalendar(context: Context): Long {
val calUri = CalendarContract.Calendars.CONTENT_URI
.buildUpon()
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
.appendQueryParameter(
CalendarContract.Calendars.ACCOUNT_NAME, CALENDAR_NAME
)
.appendQueryParameter(
CalendarContract.Calendars.ACCOUNT_TYPE,
CalendarContract.ACCOUNT_TYPE_LOCAL
)
.build()
context.contentResolver.query(
@ -140,11 +154,26 @@ object CalendarEvents {
return ContentUris.parseId(eventUriInserted)
}
/** Supprime l'événement d'agenda du traitement (si présent). */
/**
* Supprime l'événement d'agenda du traitement (si présent).
*
* ⚠️ #50 bis (v1.3.5) : CALLER_IS_SYNCADAPTER exige AUSSI les paramètres
* ACCOUNT_NAME/ACCOUNT_TYPE ici — sans eux le provider lève
* IllegalArgumentException (avalée par le catch), et l'événement restait
* dans le provider après « switch OFF » + save. Validé émulateur : la
* suppression échouait silencieusement avant ce fix.
*/
fun deleteEvent(context: Context, eventId: Long) {
val uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, eventId)
.buildUpon()
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
.appendQueryParameter(
CalendarContract.Calendars.ACCOUNT_NAME, CALENDAR_NAME
)
.appendQueryParameter(
CalendarContract.Calendars.ACCOUNT_TYPE,
CalendarContract.ACCOUNT_TYPE_LOCAL
)
.build()
try {
context.contentResolver.delete(uri, null, null)

View File

@ -110,6 +110,12 @@ fun TreatmentEditorScreen(treatmentId: Long, onDone: () -> Unit) {
var showTimePicker by remember { mutableStateOf(false) }
var loading by remember { mutableStateOf(treatmentId > 0) }
var loadedCreatedAt by remember { mutableStateOf(System.currentTimeMillis()) }
// ⚠️ BUG #51 (v1.3.5) : buildTreatment() ne transmettait PAS
// calendarEventId → l'id revenait à null à CHAQUE save : l'événement
// d'agenda devenait ORPHELIN (plus jamais supprimable/désactivable par
// l'app, il restait dans le provider). L'id existant est chargé ici,
// reporté par buildTreatment(), puis recalcité par le save (upsert).
var loadedCalendarEventId by remember { mutableStateOf<Long?>(null) }
LaunchedEffect(treatmentId) {
if (treatmentId > 0) {
@ -129,6 +135,7 @@ fun TreatmentEditorScreen(treatmentId: Long, onDone: () -> Unit) {
reminderTime = LocalTime.of(tr.reminderHour ?: 12, tr.reminderMinute ?: 0)
active = tr.isActive
loadedCreatedAt = tr.createdAt
loadedCalendarEventId = tr.calendarEventId
// Fréquence de prévision (si configurée)
tr.forecastIntervalDays?.let { fi ->
forecastEnabled = true
@ -172,6 +179,9 @@ fun TreatmentEditorScreen(treatmentId: Long, onDone: () -> Unit) {
reminderEnabled = reminderEnabled,
reminderHour = if (reminderEnabled) reminderTime.hour else null,
reminderMinute = if (reminderEnabled) reminderTime.minute else null,
// ⚠️ #51 : conserver l'id de l'événement d'agenda existant (cf
// déclaration de loadedCalendarEventId) — sinon orphelin au save.
calendarEventId = loadedCalendarEventId,
createdAt = loadedCreatedAt
)
}
@ -535,8 +545,18 @@ fun TreatmentEditorScreen(treatmentId: Long, onDone: () -> Unit) {
repo.updateTreatment(saved.copy(calendarEventId = eventToStore))
}
AppLog.log("agenda",
"traitement #${saved.id} : événement agenda " +
if (eventToStore != null) "créé/mis à jour (id=$eventToStore)" else "supprimé")
// Log VÉRIDIQUE (v1.3.5) : « supprimé » était
// imprimé aussi quand rien n'a été supprimé
// (switch désactivé, posologie absente,
// permission absente) → messages distincts.
"traitement #${saved.id} : " + when {
eventToStore != null ->
"événement agenda créé/mis à jour (id=$eventToStore)"
saved.calendarEventId != null ->
"événement agenda supprimé"
else ->
"agenda non activé (switch/posologie/permission)"
})
} catch (e: Exception) {
AppLog.log("agenda", "ERREUR événement agenda : ${e.message}")
}

View File

@ -1,7 +1,42 @@
# 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.4`, …).
Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.3.5`, …).
## [1.3.5] — 2026-09-06 (versionCode 19)
### Corrigé
- **Création du calendrier d'agenda impossible** (remontée via les logs de
diagnostic — le mécanisme v1.3.1 a tout de suite nourri le diagnostic
à distance : `Sync adapters must specify an account and account type`) :
l'URI d'insertion `CALLER_IS_SYNCADAPTER=true` du calendrier local
« HormoneTrack » n'embarquait pas `ACCOUNT_NAME` + `ACCOUNT_TYPE` —
exigé par le CalendarProvider pour toute opération sync-adapter
(`CalendarEvents.ensureCalendar`, §14 #50). Ces mêmes logs prouvent aussi
que **la permission agenda est désormais accordée** (fix manifest v1.3.3 ✓)
et que **l'export des logs fonctionne sur le téléphone** (fix #48 ✓) —
la chaîne de diagnostic est opérationnelle.
- **Suppression d'événement d'agenda silencieusement manquée** (#50 bis) :
l'URI de DELETE avait le même défaut (`CALLER_IS_SYNCADAPTER` sans params
account) → « switch OFF » + save loguait « supprimé » mais l'événement
restait dans le provider. Fix : mêmes paramètres sur l'URI de delete.
Validé émulateur : cycle complet ON→save→OFF→save = événement créé puis
réellement supprimé (`content query` vide).
- **Événement d'agenda orphelin après tout re-save** (#51) :
`TreatmentEditorScreen.buildTreatment()` ne transmettait pas
`calendarEventId` → l'id revenait à `null` à chaque sauvegarde : après le
premier save, l'événement devenait définitivement orphelin (plus jamais
supprimable/désactivable par l'app). L'id existant est désormais chargé à
l'ouverture et conservé par le save.
- **Log « événement agenda supprimé » mensonger** : imprimé aussi quand rien
n'a été supprimé (switch désactivé, posologie absente, permission absente).
Désormais trois messages distincts : « créé/mis à jour (id=…) »,
« supprimé », « non activé (switch/posologie/permission) ».
### Ajouté
- Validation émulateur de l'agenda end-to-end (recette §16.ter, permissions
accordées via `pm grant`) : calendrier + événement récurrent créés et
visibles dans le provider (`content query`).
## [1.3.4] — 2026-09-06 (versionCode 18)

View File

@ -81,6 +81,7 @@ Fonctionnalités v1 :
| 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.4) | **Deux crashs v1.3.3 remontés (Doses + export logs)** → reproduits sur ÉMULATEUR avec les vraies données (recette §16.ter, désormais standard) : (a) Doses = string `hrt_duration` 3 placeholders vs 2 args (**né en v1.3.1**, §14 #47) ; (b) export logs = `LocalDate.format("…-HHmm")` levant `UnsupportedTemporalTypeException` au TAP (§14 #48) — les fixes IO v1.3.2/v1.3.3 étaient à côté du vrai problème. Lint mis en filet bloquant (aurait attrapé les deux) + #49 notif ; helper `ExportFileNames` + 3 tests (90 verts) ; fixes VÉRIFIÉS sur émulateur avec l'APK release + données réelles (flux SAF complet : SAVE → fichier + message). Publication v1.3.4. |
| 6 sept. 2026 (session v1.3.5) | **Premier diagnostic à distance RÉUSSI via les logs exportés** : les AppLog de la v1.3.4 fournis par l'utilisatrice prouvent export logs `ok=true` (fix #48 ✓ au téléphone), permission agenda accordée (fix manifest ✓) et révèlent le bug racine EXPLICITE : `ensureCalendar` plantait car l'URI sync-adapter n'embarquait pas ACCOUNT_NAME/ACCOUNT_TYPE (§14 #50). En validant sur émulateur, DEUX bugs découverts : delete d'event sans account (#50 bis — « supprimé » logué mais l'event restait) et `buildTreatment` sans `calendarEventId` (#51 — événements orphelins). Cycle ON→save→OFF→save validé end-to-end (`content query`). Publication v1.3.5. |
| 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. |
@ -879,6 +880,48 @@ aucun contexte, dans l'esprit de cette doc) :**
théoriques `NonObservableLocale`/`LocalContextGetResourceValueCall` sont
tombés en warning via `app/lint.xml`, cf §19).
**Session v1.3.5 (diagnostic à distance via les logs exportés — la boucle par
l'exemple) :**
50. **Création du calendrier d'agenda impossible** (`IllegalArgumentException:
Sync adapters must specify an account and account type`, remontée par les
AppLog exportés du Pixel 9 v1.3.4) : `CalendarEvents.ensureCalendar`
construisait l'URI calendars avec `CALLER_IS_SYNCADAPTER=true` SANS
`ACCOUNT_NAME`+`ACCOUNT_TYPE` — requis par le CalendarProvider pour TOUTE
opération sync-adapter (requête ET insertion). De plus :
- les mêmes logs CONFIRMAIENT au passage les fixes précédents : export
logs `ok=true` (#48 ✓ depuis v1.3.4 au téléphone), permission agenda
accordée (manifest v1.3.3 ✓) — AppLog fait son job de diagnostic à
distance ;
- le log « événement agenda supprimé » était imprimé aussi quand le
switch était inéligible (posologie absente, permission absente,
switch off) → trois messages distincts désormais (créé-mis à jour /
supprimé / non activé) ;
- contexte : les « événement agenda supprimé » répétés dans les logs =
des saves où le switch ne remplissait pas les conditions d'activation
(l'utilisatrice avait retourné le switch après les échecs) — les trois
messages distincts rendront les prochains logs univoques.
FIX : append ACCOUNT_NAME/ACCOUNT_TYPE sur l'URI ; le pattern complet :
`syncadapter=true` + account + account_type sur les URIs de CRÉATION
(calendriers) ; l'insertion d'EVENTS l'avait déjà (upsertEvent). Validé
émulateur : `content query --uri content://com.android.calendar/calendars`
montre le calendrier, events avec RRULE corrects.
50.bis. **Delete d'événement silencieusement manqué** (cité au #50) : le même
défaut (CALLER_IS_SYNCADAPTER sans account) sur l'URI de DELETE de
`deleteEvent` — l'exception est avalée par le catch, le log dit
« supprimé » mais l'event reste dans le provider. FIX : mêmes params
sur l'URI. Règle : TOUT URI avec CALLER_IS_SYNCADAPTER doit embarquer
account+account_type — pas seulement les insertions.
51. **Événement orphelin après re-save** (découvert en validant #50 sur
émulateur — le provider montrait un event que la base ne référençait
plus) : `buildTreatment()` reconstruisait le Treatment SANS le champ
`calendarEventId` → null à chaque save → l'event existant devenait
orphelin dès la deuxième sauvegarde, plus jamais désactivable. FIX :
`loadedCalendarEventId` chargé dans le LaunchedEffect + renvoyé par
buildTreatment(). Leçon : la reconstruction complete d'une entité pour
un save doit repartir des champs NON ÉDITABLES (voir `loadedCreatedAt`,
déjà protégé, et désormais `loadedCalendarEventId`).
**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
@ -1274,14 +1317,22 @@ Sur le téléphone de test (à compléter par l'utilisatrice) :
(lint #49, ex format « 4.0 »)
- [ ] **v1.3.4** : `./gradlew lint` vert — intégré à la vérification de
release (step 2 de la checklist §16 désormais : tests + lint)
- [ ] **v1.3.5** : **agenda end-to-end** : sur un traitement actif avec
Posologie, activer « Événement d'agenda récurrent » → permission
accordée → sauvegarder → l'événement existe dans le calendrier local
« HormoneTrack » (« créé/mis à jour (id=…) » dans les logs exportés) ;
puis **désactiver le switch → re-save → l'événement disparaît
RÉELLEMENT** de l'app d'agenda (bugs #50 / #50 bis / #51 : en v1.3.4
ni création ni suppression ne fonctionnaient, et les re-saves
orphelinaient les événements — cf §14)
---
*Doc mise à jour le 6 sept. 2026 (v1.3.4) — build OK, lint vert, 90/90 tests verts (72 sans
*Doc mise à jour le 6 sept. 2026 (v1.3.5) — build OK, lint vert, 90/90 tests verts (72 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). v1.3.4 :
2 crashs remontés reproduits sur émulateur avec les données réelles (recette §16.ter,
scripts/seed-emulator.py versionné) — bugs #47 (stringResource arity, né v1.3.1) et #48
(LocalDate + pattern horaire au tap), lint mis en filet bloquant (#49, app/lint.xml),
ExportFileNames + 3 tests. Dette connue : fragments de labs dans l'historique git
de santé dans le dépôt ni l'historique. Fil des corrections : v1.3.3 = reprise de
maintenance (permissions agenda, export logs IO, bump de version) ; v1.3.4 = 2 crashs
reproduits sur émulateur avec les données réelles (#47 stringResource arity, #48
LocalDate+pattern horaire), lint filet bloquant ; v1.3.5 = premier diagnostic à distance
RéUSSI via les logs exportés de l'utilisatrice (#50 CalendarProvider sync-adapter account,
log agenda véridique). Dette connue : fragments de labs dans l'historique git
(v1.1.0→v1.2.3) — cf §8.bis.*

View File

@ -86,9 +86,12 @@ Les notifications de l'app remontent automatiquement sur la montre via **Huawei
passe-le **inactif** (switch « Actif ») : il disparaît de la saisie et de tes
rappels, mais **son historique reste simulé et calibré** sur les graphiques —
parfait pour une transition valerate → enanthate
- **Agenda** : active « Événement d'agenda récurrent » → un événement se
- **Agenda** : active « Événement d'agenda récurrent » (sous Rappels) → une
fois la permission accordée et le traitement sauvegardé, un événement se
répétant tous les N jours (Posologie) apparaît dans le calendrier
« HormoneTrack » de ton téléphone
« HormoneTrack » de ton téléphone (≥ v1.3.5 ; en v1.3.4 la création du
calendrier échouait — retour dans le guide si besoin : re-sauvegarde avec
le switch activé)
- Ester (EV / EU / EEn) + modèle (Estrannaise / Transfem Science) = les courbes du `.ods`
- Pour gel/patch/oral : paramètres Bateman (temps au pic, demi-vie, biodispo)
2. **Doses → +** → logue tes injections passées