v1.10.1 : durcissement rappels — singleTop (onNewIntent vivant), instant planifié dans la garde, BootReceiver goAsync
- launchMode="singleTop" sur MainActivity : un tap notification app-ouverte délivre onNewIntent SANS recréer l'activité (l'ancien standard+CLEAR_TOP recréait tout et rendait le chemin v1.10.0 inopérant — code mort). - EXTRA_PLANNED_AT : l'instant planifié voyage dans l'alarme (scheduleFor), le snooze le forward, le garde « créneau déjà honoré » évalue le jour du CRÉNEAU et non celui du feu (alarme inexacte/Doze glissant après minuit). - BootReceiver : goAsync + coroutine (dette §20.bis #2), runBlocking supprimés. - Audit doc : comptes résiduels 223 → 259 (§14 #64, §16.ter) ; §9/§20.bis à jour. 259 tests JVM + 14 UI + lint verts ; validations émulateur sur APK RELEASE avec données réelles seedées (singleTop 0 recreation, planned_at A/B, 0 crash). Web non concerné (aucune logique miroir touchée).
This commit is contained in:
parent
0bb259d12d
commit
d531ecae24
@ -33,8 +33,8 @@ android {
|
|||||||
// tag : v1.3.0→v1.3.2 contenaient tous versionCode 14 / "1.3.0"
|
// tag : v1.3.0→v1.3.2 contenaient tous versionCode 14 / "1.3.0"
|
||||||
// (bump jamais commité) → BuildConfig.VERSION_NAME était faux dans
|
// (bump jamais commité) → BuildConfig.VERSION_NAME était faux dans
|
||||||
// les APK publiés (Paramètres + titre du dialog « Nouveautés »).
|
// les APK publiés (Paramètres + titre du dialog « Nouveautés »).
|
||||||
versionCode = 46
|
versionCode = 47
|
||||||
versionName = "1.10.0"
|
versionName = "1.10.1"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|||||||
@ -26,7 +26,13 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
android:launchMode="singleTop"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<!-- launchMode singleTop (v1.10.1) : un tap sur la notification de
|
||||||
|
rappel quand l'app est DÉJÀ ouverte délivre onNewIntent (avec les
|
||||||
|
extras open_log_dose/treatment_id) SANS détruire/recréer
|
||||||
|
l'activité — l'ancien comportement (standard + CLEAR_TOP)
|
||||||
|
recréait tout, et onNewIntent était du code mort. -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import androidx.activity.enableEdgeToEdge
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.core.os.LocaleListCompat
|
import androidx.core.os.LocaleListCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.hormonetrack.ui.HormoneTrackRoot
|
import com.hormonetrack.ui.HormoneTrackRoot
|
||||||
@ -31,7 +32,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
* Accueil détruisait les `remember` locaux, le paramètre lui vivait au
|
* Accueil détruisait les `remember` locaux, le paramètre lui vivait au
|
||||||
* niveau activité → le dialog se rouvrait à CHAQUE retour sur l'accueil).
|
* niveau activité → le dialog se rouvrait à CHAQUE retour sur l'accueil).
|
||||||
*/
|
*/
|
||||||
private val logDoseRequest = androidx.compose.runtime.mutableStateOf<Long?>(null)
|
private val logDoseRequest = mutableStateOf<Long?>(null)
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|||||||
@ -43,7 +43,14 @@ class DoseActionReceiver : BroadcastReceiver() {
|
|||||||
ReminderContract.ACTION_SNOOZE -> {
|
ReminderContract.ACTION_SNOOZE -> {
|
||||||
val name = intent.getStringExtra(ReminderContract.EXTRA_TREATMENT_NAME) ?: ""
|
val name = intent.getStringExtra(ReminderContract.EXTRA_TREATMENT_NAME) ?: ""
|
||||||
val dose = intent.getDoubleExtra(ReminderContract.EXTRA_DOSE_AMOUNT, 0.0)
|
val dose = intent.getDoubleExtra(ReminderContract.EXTRA_DOSE_AMOUNT, 0.0)
|
||||||
AlarmScheduler(context).scheduleSnooze(treatmentId, name, dose)
|
// v1.10.1 : le snooze FORWARDE l'instant planifié d'origine —
|
||||||
|
// le garde du feu différé évalue le jour du CRÉNEAU, pas
|
||||||
|
// celui du snooze (sinon un snooze à cheval sur minuit
|
||||||
|
// évaluerait le mauvais jour).
|
||||||
|
val plannedAt = if (intent.hasExtra(ReminderContract.EXTRA_PLANNED_AT)) {
|
||||||
|
intent.getLongExtra(ReminderContract.EXTRA_PLANNED_AT, 0L)
|
||||||
|
} else null
|
||||||
|
AlarmScheduler(context).scheduleSnooze(treatmentId, name, dose, plannedAt = plannedAt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,7 +20,6 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
|
|
||||||
object ReminderContract {
|
object ReminderContract {
|
||||||
const val CHANNEL_ID = "hormonetrack_reminders"
|
const val CHANNEL_ID = "hormonetrack_reminders"
|
||||||
@ -31,6 +30,17 @@ object ReminderContract {
|
|||||||
const val EXTRA_TREATMENT_NAME = "treatment_name"
|
const val EXTRA_TREATMENT_NAME = "treatment_name"
|
||||||
const val EXTRA_DOSE_AMOUNT = "dose_amount"
|
const val EXTRA_DOSE_AMOUNT = "dose_amount"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instant PLANIFIÉ de l'alarme (v1.10.1) — embarqué par
|
||||||
|
* AlarmScheduler.scheduleFor au moment du scheduling et relu par
|
||||||
|
* [ReminderReceiver]. ⚠️ Nécessaire car une alarme INEXACTE (setWindow
|
||||||
|
* sans SCHEDULE_EXACT_ALARM) ou retardée (Doze) peut fire après la
|
||||||
|
* frontière de jour : le garde « créneau déjà honoré » doit évaluer le
|
||||||
|
* jour PLANIFIÉ, pas le jour réel du feu. Absent (vieux PendingIntents)
|
||||||
|
* → le receiver retombe sur l'heure courante.
|
||||||
|
*/
|
||||||
|
const val EXTRA_PLANNED_AT = "planned_at"
|
||||||
|
|
||||||
fun reminderIntent(context: Context, treatmentId: Long): Intent =
|
fun reminderIntent(context: Context, treatmentId: Long): Intent =
|
||||||
Intent(context, ReminderReceiver::class.java).apply {
|
Intent(context, ReminderReceiver::class.java).apply {
|
||||||
action = ACTION_REMINDER
|
action = ACTION_REMINDER
|
||||||
@ -54,10 +64,16 @@ class AlarmScheduler(private val context: Context) {
|
|||||||
|
|
||||||
private val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
private val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||||
|
|
||||||
private fun pendingBroadcast(treatmentId: Long, name: String?, dose: Double?): PendingIntent {
|
private fun pendingBroadcast(
|
||||||
|
treatmentId: Long,
|
||||||
|
name: String?,
|
||||||
|
dose: Double?,
|
||||||
|
plannedAt: Long? = null
|
||||||
|
): PendingIntent {
|
||||||
val intent = ReminderContract.reminderIntent(context, treatmentId)
|
val intent = ReminderContract.reminderIntent(context, treatmentId)
|
||||||
name?.let { intent.putExtra(ReminderContract.EXTRA_TREATMENT_NAME, it) }
|
name?.let { intent.putExtra(ReminderContract.EXTRA_TREATMENT_NAME, it) }
|
||||||
dose?.let { intent.putExtra(ReminderContract.EXTRA_DOSE_AMOUNT, it) }
|
dose?.let { intent.putExtra(ReminderContract.EXTRA_DOSE_AMOUNT, it) }
|
||||||
|
plannedAt?.let { intent.putExtra(ReminderContract.EXTRA_PLANNED_AT, it) }
|
||||||
return PendingIntent.getBroadcast(
|
return PendingIntent.getBroadcast(
|
||||||
context,
|
context,
|
||||||
treatmentId.toInt(),
|
treatmentId.toInt(),
|
||||||
@ -81,12 +97,27 @@ class AlarmScheduler(private val context: Context) {
|
|||||||
val fireAt = PharmacokineticEngine.nextReminderFireFor(
|
val fireAt = PharmacokineticEngine.nextReminderFireFor(
|
||||||
treatment, doseLogs, System.currentTimeMillis()
|
treatment, doseLogs, System.currentTimeMillis()
|
||||||
) ?: return false
|
) ?: return false
|
||||||
val pi = pendingBroadcast(treatment.id, treatment.name, treatment.doseAmount)
|
// L'instant planifié voyage dans l'intent : le garde « créneau déjà
|
||||||
|
// honoré » (v1.10.1) évalue le jour PLANIFIÉ, pas celui du feu réel
|
||||||
|
// (une alarme inexacte peut glisser après minuit — cf
|
||||||
|
// ReminderContract.EXTRA_PLANNED_AT).
|
||||||
|
val pi = pendingBroadcast(treatment.id, treatment.name, treatment.doseAmount, fireAt)
|
||||||
return scheduleAt(fireAt, pi)
|
return scheduleAt(fireAt, pi)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun scheduleSnooze(treatmentId: Long, name: String, dose: Double, delayMs: Long = 60 * 60_000L): Boolean {
|
/**
|
||||||
val pi = pendingBroadcast(treatmentId, name, dose)
|
* Alarme « Reporter 1 h ». [plannedAt] : l'instant PLANIFIÉ du créneau
|
||||||
|
* d'origine — forwardé tel quel pour que le garde du feu différé
|
||||||
|
* évalue toujours le bon jour (et non le jour du snooze).
|
||||||
|
*/
|
||||||
|
fun scheduleSnooze(
|
||||||
|
treatmentId: Long,
|
||||||
|
name: String,
|
||||||
|
dose: Double,
|
||||||
|
delayMs: Long = 60 * 60_000L,
|
||||||
|
plannedAt: Long? = null
|
||||||
|
): Boolean {
|
||||||
|
val pi = pendingBroadcast(treatmentId, name, dose, plannedAt)
|
||||||
return scheduleAt(System.currentTimeMillis() + delayMs, pi)
|
return scheduleAt(System.currentTimeMillis() + delayMs, pi)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,19 +248,27 @@ class ReminderReceiver : BroadcastReceiver() {
|
|||||||
// (prise saisie avant l'heure de rappel), la notification est SAUTÉE —
|
// (prise saisie avant l'heure de rappel), la notification est SAUTÉE —
|
||||||
// elle ne doit pas sonner pour une prise déjà faite — et le rappel
|
// elle ne doit pas sonner pour une prise déjà faite — et le rappel
|
||||||
// suivant repart au créneau suivant (re-schedule dans tous les cas).
|
// suivant repart au créneau suivant (re-schedule dans tous les cas).
|
||||||
|
// v1.10.1 : le jour évalué est celui du créneau PLANIFIÉ
|
||||||
|
// (EXTRA_PLANNED_AT), pas celui du feu réel — une alarme inexacte
|
||||||
|
// peut glisser après minuit sans changer de créneau.
|
||||||
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
val plannedAtMs = if (intent.hasExtra(ReminderContract.EXTRA_PLANNED_AT)) {
|
||||||
|
intent.getLongExtra(ReminderContract.EXTRA_PLANNED_AT, 0L)
|
||||||
|
} else {
|
||||||
|
System.currentTimeMillis()
|
||||||
|
}
|
||||||
val pending = goAsync()
|
val pending = goAsync()
|
||||||
CoroutineScope(SupervisorJob() + Dispatchers.IO).launch {
|
CoroutineScope(SupervisorJob() + Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
val db = AppDatabase.getInstance(context)
|
val db = AppDatabase.getInstance(context)
|
||||||
val doses = db.doseLogDao().getAllOnce()
|
val doses = db.doseLogDao().getAllOnce()
|
||||||
if (PharmacokineticEngine.hasDoseLoggedOnDay(
|
if (PharmacokineticEngine.hasDoseLoggedOnDay(
|
||||||
treatmentId, doses, System.currentTimeMillis()
|
treatmentId, doses, plannedAtMs
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
AppLog.log(
|
AppLog.log(
|
||||||
"reminder",
|
"reminder",
|
||||||
"créneau déjà honoré (dose loguée aujourd'hui) → notification sautée (treatment=$treatmentId)"
|
"créneau déjà honoré (dose loguée le jour planifié) → notification sautée (treatment=$treatmentId)"
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
manager.notify(treatmentId.toInt(), notification)
|
manager.notify(treatmentId.toInt(), notification)
|
||||||
@ -247,23 +286,23 @@ class ReminderReceiver : BroadcastReceiver() {
|
|||||||
class BootReceiver : BroadcastReceiver() {
|
class BootReceiver : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
if (intent.action != Intent.ACTION_BOOT_COMPLETED) return
|
if (intent.action != Intent.ACTION_BOOT_COMPLETED) return
|
||||||
val result = goAsync()
|
// v1.10.1 : consolidation goAsync + coroutine (pattern ReminderReceiver —
|
||||||
Thread {
|
// remplace le Thread + double runBlocking de la v1.0, dette §20.bis #2) :
|
||||||
|
// une seule lecture séquentielle des DAO dans un scope IO dédié, finish()
|
||||||
|
// garanti par le finally.
|
||||||
|
val pending = goAsync()
|
||||||
|
CoroutineScope(SupervisorJob() + Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
val db = AppDatabase.getInstance(context)
|
val db = AppDatabase.getInstance(context)
|
||||||
// TOUS les traitements : les inactifs doivent voir leur alarme
|
// TOUS les traitements : les inactifs doivent voir leur alarme
|
||||||
// annulée au boot (sinon une alarme périmée continue de sonner) ;
|
// annulée au boot (sinon une alarme périmée continue de sonner) ;
|
||||||
// les doses alimentent la grille Posologie (fix #52)
|
// les doses alimentent la grille Posologie (fix #52)
|
||||||
val treatments = runBlocking {
|
val treatments = db.treatmentDao().getAllOnce()
|
||||||
db.treatmentDao().getAllOnce()
|
val doses = db.doseLogDao().getAllOnce()
|
||||||
}
|
|
||||||
val doses = runBlocking {
|
|
||||||
db.doseLogDao().getAllOnce()
|
|
||||||
}
|
|
||||||
AlarmScheduler(context).rescheduleAll(treatments, doses)
|
AlarmScheduler(context).rescheduleAll(treatments, doses)
|
||||||
} finally {
|
} finally {
|
||||||
result.finish()
|
pending.finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.start()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,31 @@
|
|||||||
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.4.10`, …).
|
Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.4.10`, …).
|
||||||
|
|
||||||
|
## [1.10.1] — 2026-09-28 (versionCode 47)
|
||||||
|
|
||||||
|
### Amélioré — durcissement du système de rappels (revue de code post-v1.10.0)
|
||||||
|
|
||||||
|
- **`launchMode="singleTop"` sur MainActivity** : un tap sur la notification
|
||||||
|
quand l'app est DÉJÀ ouverte délivre désormais `onNewIntent` (dialog de
|
||||||
|
saisie ouvert, extras traités) **sans détruire/recréer l'activité** —
|
||||||
|
l'ancien combo `standard` + `CLEAR_TOP` recréait tout et rendait le chemin
|
||||||
|
`onNewIntent` de la v1.10.0 inopérant (code mort).
|
||||||
|
- **Instant PLANIFIÉ dans l'alarme (`EXTRA_PLANNED_AT`)** : le garde
|
||||||
|
« créneau déjà honoré » (v1.10.0) évaluait le jour au moment du FEU — une
|
||||||
|
alarme inexacte (`setWindow`) ou retardée (Doze) glissant après minuit
|
||||||
|
évaluait le mauvais jour (23:58 → 00:02). L'instant planifié voyage
|
||||||
|
désormais dans l'intent de l'alarme, le snooze le FORWARDE tel quel, et
|
||||||
|
le garde évalue toujours le jour du CRÉNEAU. Vérifié sur émulateur par
|
||||||
|
broadcast avec `planned_at` daté d'hier → notification correctement
|
||||||
|
sautée.
|
||||||
|
- **`BootReceiver` consolidé** (dette §20.bis #2) : `goAsync` + coroutine IO
|
||||||
|
(pattern ReminderReceiver) — remplace le `Thread` + double `runBlocking`
|
||||||
|
de la v1.0 ; une seule lecture séquentielle des DAO, `finish()` garanti.
|
||||||
|
- Audit doc : comptes de tests périmés corrigés (§14 note #64, §16.ter).
|
||||||
|
|
||||||
|
Vérifié : 259 tests JVM + 14 tests UI + lint verts ; scénarios émulateur
|
||||||
|
(singleTop + planned_at). Web non concerné (aucune logique miroir touchée).
|
||||||
|
|
||||||
## [1.10.0] — 2026-09-27 (versionCode 46)
|
## [1.10.0] — 2026-09-27 (versionCode 46)
|
||||||
|
|
||||||
### Corrigé — la recommandation de prise de sang précédait la stabilisation (#68)
|
### Corrigé — la recommandation de prise de sang précédait la stabilisation (#68)
|
||||||
|
|||||||
@ -63,6 +63,7 @@ Fonctionnalités v1 :
|
|||||||
|
|
||||||
| Date | Événement |
|
| Date | Événement |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
| 28 sept. 2026 (session v1.10.1) | **Durcissement rappels — revue de code post-v1.10.0** (audit demandé : « optimisations ou problèmes, web et Android »). (a) **`launchMode="singleTop"`** : le combo `standard`+`CLEAR_TOP` recréait l'activité au tap notification app-ouverte et rendait le `onNewIntent` de la v1.10.0 inopérant (code mort) — désormais délivré SANS recreation (état UI préservé). (b) **`EXTRA_PLANNED_AT`** : le garde « créneau déjà honoré » évaluait le jour au moment du FEU — une alarme inexacte (`setWindow`/Doze) glissant 23:58→00:02 évaluait le mauvais jour ; l'instant planifié voyage dans l'intent (scheduleFor), le snooze le FORWARDE, le garde évalue le jour du CRÉNEAU ; vérifié émulateur (broadcast `planned_at` hier → notif sautée). (c) **`BootReceiver` consolidé** (dette §20.bis #2 FAIT) : `goAsync`+coroutine, `runBlocking` supprimés. (d) Style (import `mutableStateOf`) + audit doc (comptes 223 résiduels §14/#64 et §16.ter → 259 ; §9 et §20.bis à jour). Web NON concerné (miroir lab-timing laissé tel quel — boucle correcte et testée ; `importJson` par lots reste en dette). 259 tests JVM + 14 UI + lint verts. |
|
||||||
| 27-28 sept. 2026 (session v1.10.0) | **2 fixes + 1 amélioration rappels** (remontées + nouvelles données réelles → 6ᵉ régression épinglée `backup-v1.9.8.json`). (a) **#68 — reco de prise de sang antérieure à la stabilisation** : le « creux » = minimum de la FENÊTRE ENTIÈRE tombait juste après l'injection PRÉCÉDENTE pour les esters à montée lente (EEn pic ~J+5 ≈ intervalle 7 j → creux d'absorption ~3 h post-injection) — date gouvernée par l'injection N−1 (antérieure à la stab) et « juste avant ton injection du … » faux de plusieurs jours (réel : creux 5 oct 02:32 < stab 7 oct 04:03). FIX : creux = niveau PRÉ-INJECTION du créneau (restaure le principe v1.8.0 documenté ; EV inchangé — le point pré-injection EST son min de fenêtre) ; **miroir web appliqué** (lab-timing.js). Tests : RegressionUserCase6Test data-driven dont balayage de `now` ±60 j (invariant « creux ≥ stab » à TOUT INSTANT) ; les 17 LabTimingTest passent inchangés. (b) **#69 — dialog fantôme** : tap notification → dialog ; le fermer + changer d'onglet + revenir sur l'accueil le ROUVRAIT à chaque fois (paramètre d'activité vs consommation dans les remember de Home, détruits à chaque sortie d'onglet — reproduit sur émulateur AVANT fix). FIX : `MutableState<Long?>` détenu par MainActivity, consommé UNE fois (extras retirés de l'intent après lecture ; bonus : tap notification app-ouverte ouvre aussi le dialog). Test UI `LogDoseRequestTest` + vérification émulateur. (c) **Rappel sauté si dose déjà loggée** (demande) : garde au déclenchement — `hasDoseLoggedOnDay` (pur, jour calendaire LOCAL) dans ReminderReceiver → notif sautée + re-programmation créneau suivant dans tous les cas ; 6 tests JVM + vérification A/B émulateur (EEn dose-du-jour → 0 notif, Fluoxetine sans dose → 1 notif). **259 tests JVM + 14 UI + lint** ; **web 180 tests + E2E verts** (check.sh). **Publication** : commit a4f859f + tag v1.10.0 poussés sur les 2 Gitea, APK release (2,7 Mo) + debug (21 Mo) publiés et vérifiés par téléchargement (cloudyfy id 77, farewell id 1588583). **Web miroir v1.10.0 sync** (fix #68 lab-timing, WEB_VERSION alignée, commit b865b16 + tag poussés ; #69/garde rappel = Android-only, notés dans le changelog web). |
|
| 27-28 sept. 2026 (session v1.10.0) | **2 fixes + 1 amélioration rappels** (remontées + nouvelles données réelles → 6ᵉ régression épinglée `backup-v1.9.8.json`). (a) **#68 — reco de prise de sang antérieure à la stabilisation** : le « creux » = minimum de la FENÊTRE ENTIÈRE tombait juste après l'injection PRÉCÉDENTE pour les esters à montée lente (EEn pic ~J+5 ≈ intervalle 7 j → creux d'absorption ~3 h post-injection) — date gouvernée par l'injection N−1 (antérieure à la stab) et « juste avant ton injection du … » faux de plusieurs jours (réel : creux 5 oct 02:32 < stab 7 oct 04:03). FIX : creux = niveau PRÉ-INJECTION du créneau (restaure le principe v1.8.0 documenté ; EV inchangé — le point pré-injection EST son min de fenêtre) ; **miroir web appliqué** (lab-timing.js). Tests : RegressionUserCase6Test data-driven dont balayage de `now` ±60 j (invariant « creux ≥ stab » à TOUT INSTANT) ; les 17 LabTimingTest passent inchangés. (b) **#69 — dialog fantôme** : tap notification → dialog ; le fermer + changer d'onglet + revenir sur l'accueil le ROUVRAIT à chaque fois (paramètre d'activité vs consommation dans les remember de Home, détruits à chaque sortie d'onglet — reproduit sur émulateur AVANT fix). FIX : `MutableState<Long?>` détenu par MainActivity, consommé UNE fois (extras retirés de l'intent après lecture ; bonus : tap notification app-ouverte ouvre aussi le dialog). Test UI `LogDoseRequestTest` + vérification émulateur. (c) **Rappel sauté si dose déjà loggée** (demande) : garde au déclenchement — `hasDoseLoggedOnDay` (pur, jour calendaire LOCAL) dans ReminderReceiver → notif sautée + re-programmation créneau suivant dans tous les cas ; 6 tests JVM + vérification A/B émulateur (EEn dose-du-jour → 0 notif, Fluoxetine sans dose → 1 notif). **259 tests JVM + 14 UI + lint** ; **web 180 tests + E2E verts** (check.sh). **Publication** : commit a4f859f + tag v1.10.0 poussés sur les 2 Gitea, APK release (2,7 Mo) + debug (21 Mo) publiés et vérifiés par téléchargement (cloudyfy id 77, farewell id 1588583). **Web miroir v1.10.0 sync** (fix #68 lab-timing, WEB_VERSION alignée, commit b865b16 + tag poussés ; #69/garde rappel = Android-only, notés dans le changelog web). |
|
||||||
| 22 sept. 2026 (session v1.9.8) | **Socle qualité : tests UI Compose + signature release dédiée + découpage Settings** (revue de code du 21 sept. : « pas de CI, pas de tests UI, APK signé clé debug, écrans qui grandissent »). (a) **13 tests UI instrumentés** (`app/src/androidTest/uitest/`, §8.ter) : navigation 5 onglets (preuves = éléments inconditionnels : chips 24 h, FAB), dialog Nouveautés (épinglage du fix v1.3.2 : ne réapparaît pas après dismiss+restart), sections Paramètres + version installée = BuildConfig (le bug #46 ne peut plus revenir) ; déterminisme = pm grant shell pré-launch + markChangelogSeen + assertions via targetContext (indépendant FR/EN) + zéro dépendance aux données Room ; deps androidTest (BOM + ui-test-junit4 + ext:junit-ktx + junit explicite). (b) **Signature release conditionnelle** (§16.quater) : `scripts/make-release-keystore.sh` (shellcheck+shfmt) génère keystore + keystore.properties (gitignorés), `assembleRelease` l'utilise s'il existe, sinon fallback clé debug inchangé ; migration backup→désinstall→réimport documentée. (c) **SettingsScreen découpé** (840 → 377 + 11 fichiers 33–164 lignes dans `ui/screens/settings/`, §10) — déplacement pur, commentaires historiques conservés, ChartScreen volontairement entier (garde de source #54). **Vérifications** : 249 tests JVM + 13 UI verts, lint vert, assembleDebug/Release OK, **smoke test RELEASE émulateur** (accueil, 5 onglets, Paramètres complet scroll compris — version 1.9.8 visible, éditeur traitement, dialog dose + garde « crée un traitement », 0 crash logcat — anti-#64). **README EN** (README.en.md, liens FR/EN croisés ; DEVELOPPEMENT.md reste FR par choix assumé). **Publication** : commit 09cddfc + tag v1.9.8 poussés sur les 2 Gitea, APK release (2,7 Mo) + debug (21 Mo) publiés et vérifiés par téléchargement (cloudyfy id 75, farewell id 1588581). Web miroir NON concerné (v1.9.8 = socle Android uniquement : tests instrumentés, signature APK, découpage Compose, README du dépôt Android — rien à porter côté web, qui reste à v1.9.7). |
|
| 22 sept. 2026 (session v1.9.8) | **Socle qualité : tests UI Compose + signature release dédiée + découpage Settings** (revue de code du 21 sept. : « pas de CI, pas de tests UI, APK signé clé debug, écrans qui grandissent »). (a) **13 tests UI instrumentés** (`app/src/androidTest/uitest/`, §8.ter) : navigation 5 onglets (preuves = éléments inconditionnels : chips 24 h, FAB), dialog Nouveautés (épinglage du fix v1.3.2 : ne réapparaît pas après dismiss+restart), sections Paramètres + version installée = BuildConfig (le bug #46 ne peut plus revenir) ; déterminisme = pm grant shell pré-launch + markChangelogSeen + assertions via targetContext (indépendant FR/EN) + zéro dépendance aux données Room ; deps androidTest (BOM + ui-test-junit4 + ext:junit-ktx + junit explicite). (b) **Signature release conditionnelle** (§16.quater) : `scripts/make-release-keystore.sh` (shellcheck+shfmt) génère keystore + keystore.properties (gitignorés), `assembleRelease` l'utilise s'il existe, sinon fallback clé debug inchangé ; migration backup→désinstall→réimport documentée. (c) **SettingsScreen découpé** (840 → 377 + 11 fichiers 33–164 lignes dans `ui/screens/settings/`, §10) — déplacement pur, commentaires historiques conservés, ChartScreen volontairement entier (garde de source #54). **Vérifications** : 249 tests JVM + 13 UI verts, lint vert, assembleDebug/Release OK, **smoke test RELEASE émulateur** (accueil, 5 onglets, Paramètres complet scroll compris — version 1.9.8 visible, éditeur traitement, dialog dose + garde « crée un traitement », 0 crash logcat — anti-#64). **README EN** (README.en.md, liens FR/EN croisés ; DEVELOPPEMENT.md reste FR par choix assumé). **Publication** : commit 09cddfc + tag v1.9.8 poussés sur les 2 Gitea, APK release (2,7 Mo) + debug (21 Mo) publiés et vérifiés par téléchargement (cloudyfy id 75, farewell id 1588581). Web miroir NON concerné (v1.9.8 = socle Android uniquement : tests instrumentés, signature APK, découpage Compose, README du dépôt Android — rien à porter côté web, qui reste à v1.9.7). |
|
||||||
| 20 sept. 2026 (session v1.9.7) | **Fix « la suggestion de prise de sang change tout le temps »** (remontée + données réelles : logs d'injection à heures variables) : la règle de stabilisation v1.8.1 comparait l'écart inter-doses EXACTEMENT à l'écart précédent — un log 30 min plus tard cassait le régime et repoussait la stabilisation de 5 × t½ à CHAQUE injection (creux fuyant). FIX : l'écart doit rester dans **l'intervalle de Posologie ± 24 h** (`GAP_TOLERANCE_MS`, fenêtre vs l'interval THÉORIQUE — des logs 6,8 j puis 7,2 j ne se déstabilisent plus en cascade ; un vrai passage à 2 j reste hors fenêtre). Le créneau affiché suit toujours la dernière dose réelle (voulu). Tests : « interval change » ré-épinglé + 2 tests de tolérance. 249 verts + lint. Web miroir v1.9.7 (12 verts lab-timing). §7.11 mis à jour. |
|
| 20 sept. 2026 (session v1.9.7) | **Fix « la suggestion de prise de sang change tout le temps »** (remontée + données réelles : logs d'injection à heures variables) : la règle de stabilisation v1.8.1 comparait l'écart inter-doses EXACTEMENT à l'écart précédent — un log 30 min plus tard cassait le régime et repoussait la stabilisation de 5 × t½ à CHAQUE injection (creux fuyant). FIX : l'écart doit rester dans **l'intervalle de Posologie ± 24 h** (`GAP_TOLERANCE_MS`, fenêtre vs l'interval THÉORIQUE — des logs 6,8 j puis 7,2 j ne se déstabilisent plus en cascade ; un vrai passage à 2 j reste hors fenêtre). Le créneau affiché suit toujours la dernière dose réelle (voulu). Tests : « interval change » ré-épinglé + 2 tests de tolérance. 249 verts + lint. Web miroir v1.9.7 (12 verts lab-timing). §7.11 mis à jour. |
|
||||||
@ -1092,6 +1093,12 @@ la session 1 !), plancher 0,01 des queues de profils, mapping silencieux du mod
|
|||||||
créneau SUIVANT reste exécutée dans tous les cas. Vérifié A/B sur
|
créneau SUIVANT reste exécutée dans tous les cas. Vérifié A/B sur
|
||||||
émulateur (`am broadcast -n …ReminderReceiver` : EEn avec dose du jour →
|
émulateur (`am broadcast -n …ReminderReceiver` : EEn avec dose du jour →
|
||||||
0 notif, Fluoxetine sans dose → 1 notif).
|
0 notif, Fluoxetine sans dose → 1 notif).
|
||||||
|
**v1.10.1 — instant PLANIFIÉ (`EXTRA_PLANNED_AT`)** : le garde évalue le
|
||||||
|
jour du CRÉNEAU PLANIFIÉ (embarqué dans l'intent de l'alarme par
|
||||||
|
`scheduleFor`, forwardé tel quel par le snooze), pas le jour du feu
|
||||||
|
réel — une alarme inexacte (`setWindow`) ou retardée (Doze) glissant
|
||||||
|
après minuit évaluait le mauvais jour (23:58 → 00:02). Vérifié émulateur
|
||||||
|
(broadcast avec `planned_at` daté d'hier → notification sautée).
|
||||||
- `DoseActionReceiver` (non exporté) : **« Pris »** → `goAsync()` + coroutine IO → insert
|
- `DoseActionReceiver` (non exporté) : **« Pris »** → `goAsync()` + coroutine IO → insert
|
||||||
DoseLog (dose = extra ou standard) **+ reprogramme** (la dose loguée avance
|
DoseLog (dose = extra ou standard) **+ reprogramme** (la dose loguée avance
|
||||||
la grille) ; **« Reporter 1 h »** → `scheduleSnooze` ; annule la notif
|
la grille) ; **« Reporter 1 h »** → `scheduleSnooze` ; annule la notif
|
||||||
@ -1801,7 +1808,7 @@ l'optimiseur.
|
|||||||
le chip « Lab track » s'active MAIS la courbe n'est ni dessinée (0 pixel
|
le chip « Lab track » s'active MAIS la courbe n'est ni dessinée (0 pixel
|
||||||
#C2185B au screencap) ni légendée ; « Prolonger » idem. En DEBUG, le même
|
#C2185B au screencap) ni légendée ; « Prolonger » idem. En DEBUG, le même
|
||||||
chip sur les mêmes données fonctionne (légende + 7090 px rose après fix).
|
chip sur les mêmes données fonctionne (légende + 7090 px rose après fix).
|
||||||
**Les 191 tests JVM de l'époque sont verts** (223 aujourd'hui — ils ne passent JAMAIS par R8).
|
**Les 191 tests JVM de l'époque sont verts** (259 aujourd'hui — ils ne passent JAMAIS par R8).
|
||||||
|
|
||||||
**Diagnostic** (méthode de bisection debug/release puis `mapping.txt`) :
|
**Diagnostic** (méthode de bisection debug/release puis `mapping.txt`) :
|
||||||
`com.hormonetrack.pk.LabTrajectoryModel -> R8$$REMOVED$$CLASS$$…` — R8 full
|
`com.hormonetrack.pk.LabTrajectoryModel -> R8$$REMOVED$$CLASS$$…` — R8 full
|
||||||
@ -2050,12 +2057,13 @@ de signature est requise — elle est désormais câblée :
|
|||||||
headless hrt36 + seed `scripts/seed-emulator.py` avec le dernier
|
headless hrt36 + seed `scripts/seed-emulator.py` avec le dernier
|
||||||
backup de `local-test-data/` + APK release installé PAR-DESSUS) sur
|
backup de `local-test-data/` + APK release installé PAR-DESSUS) sur
|
||||||
**l'APK RELEASE** (JAMAIS le debug seul : **R8 ne se vérifie qu'ici** —
|
**l'APK RELEASE** (JAMAIS le debug seul : **R8 ne se vérifie qu'ici** —
|
||||||
les 223 tests JVM ne le voient jamais, cf #64). Parcours minimum :
|
les 259 tests JVM ne le voient jamais, cf #64). Parcours minimum :
|
||||||
l'app démarre 0 crash → écran Graphiques → activer les chips de la
|
l'app démarre 0 crash → écran Graphiques → activer les chips de la
|
||||||
release (rangée modèles défilable : `adb shell input swipe 900 716 150
|
release (rangée modèles défilable : `adb shell input swipe 900 716 150
|
||||||
716 400` puis tap aux bounds du `uiautomator dump`) → vérifier dans le
|
716 400` puis tap aux bounds du `uiautomator dump`) → vérifier dans le
|
||||||
dump `text=` que la légende attendue apparaît → `adb logcat -b crash
|
dump `text=` que la légende attendue apparaît → `adb logcat -b crash
|
||||||
-d | grep -c FATAL` reste à 0. Limites : le pan/pinch n'est PAS
|
-d | grep -c FATAL` reste à 0 (les **259** tests JVM ne voient jamais
|
||||||
|
R8, cf #64). Limites : le pan/pinch n'est PAS
|
||||||
testable par adb (§16.ter 4.bis) — le geste reste validé par
|
testable par adb (§16.ter 4.bis) — le geste reste validé par
|
||||||
l'utilisatrice sur Pixel 9.
|
l'utilisatrice sur Pixel 9.
|
||||||
4. **Commit** (message descriptif par couche) + **tag annoté** :
|
4. **Commit** (message descriptif par couche) + **tag annoté** :
|
||||||
@ -2288,10 +2296,9 @@ pour ne pas les redécouvrir et re-reposer les mêmes questions :
|
|||||||
Room par ligne. `@Insert` en liste préserverait l'ordre (IDs) et
|
Room par ligne. `@Insert` en liste préserverait l'ordre (IDs) et
|
||||||
accélérerait l'import d'un gros historique. Différé : surface DAO +
|
accélérerait l'import d'un gros historique. Différé : surface DAO +
|
||||||
repository à modifier, validation manuelle (aucun test Room auto).
|
repository à modifier, validation manuelle (aucun test Room auto).
|
||||||
2. **`BootReceiver` : consolidation `goAsync` + une seule lecture** :
|
2. ~~**`BootReceiver` : consolidation `goAsync` + une seule lecture`~~**
|
||||||
les deux `runBlocking` séquentiels peuvent fusionner trivialement ; le
|
**FAIT v1.10.1** (pattern `goAsync`+coroutine de ReminderReceiver,
|
||||||
passage complet au pattern `goAsync`+coroutine de ReminderReceiver est
|
`runBlocking` supprimés du fichier).
|
||||||
équivalent mais re-testé sur émulateur (boot). Différé.
|
|
||||||
3. **Double évaluation de `generateForecastDoses`** dans ChartScreen
|
3. **Double évaluation de `generateForecastDoses`** dans ChartScreen
|
||||||
(producer `forecastDoses` vs producer `curves`) : NE PAS fusionner tel
|
(producer `forecastDoses` vs producer `curves`) : NE PAS fusionner tel
|
||||||
quel — le premier utilise `System.currentTimeMillis()`, le second le
|
quel — le premier utilise `System.currentTimeMillis()`, le second le
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user