v1.3.4 : 2 crashs v1.3.3 reproduits sur émulateur (données réelles) et corrigés — #47 Doses : stringResource hrt_duration 3 placeholders vs 2 args (né v1.3.1) ; #48 export logs : LocalDate.format('…-HHmm') levait UnsupportedTemporalTypeException au tap → util ExportFileNames + 3 tests ; #49 notif dose StringFormatMatches ; lint filet bloquant (app/lint.xml) ; recette émulateur §16.ter + scripts/seed-emulator.py ; 90 tests verts
This commit is contained in:
parent
e5fefc90df
commit
71659b5647
11
README.md
11
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
|
> à titre informatif — ce ne sont pas des mesures. Fie-toi toujours à tes analyses de
|
||||||
> sang et aux consignes de ton endocrinologue.
|
> sang et aux consignes de ton endocrinologue.
|
||||||
|
|
||||||
- **Statut** : v1.3.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** ✅
|
- **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** ✅
|
||||||
- **Journal des versions** : [docs/CHANGELOG.md](docs/CHANGELOG.md)
|
- **Journal des versions** : [docs/CHANGELOG.md](docs/CHANGELOG.md)
|
||||||
- **Guide utilisateur** : [docs/GUIDE_INSTALLATION.md](docs/GUIDE_INSTALLATION.md)
|
- **Guide utilisateur** : [docs/GUIDE_INSTALLATION.md](docs/GUIDE_INSTALLATION.md)
|
||||||
- **Doc de développement** (architecture, maths, décisions, bugs) : [docs/DEVELOPPEMENT.md](docs/DEVELOPPEMENT.md)
|
- **Doc de développement** (architecture, maths, décisions, bugs) : [docs/DEVELOPPEMENT.md](docs/DEVELOPPEMENT.md)
|
||||||
@ -73,7 +73,8 @@ par AGP si les licences sont signées). Le wrapper télécharge Gradle 9.7.1.
|
|||||||
git clone <repo> && cd HormoneTrack
|
git clone <repo> && cd HormoneTrack
|
||||||
echo "sdk.dir=/chemin/vers/android-sdk" > local.properties # ou ANDROID_HOME
|
echo "sdk.dir=/chemin/vers/android-sdk" > local.properties # ou ANDROID_HOME
|
||||||
./gradlew assembleDebug # APK : app/build/outputs/apk/debug/app-debug.apk
|
./gradlew assembleDebug # APK : app/build/outputs/apk/debug/app-debug.apk
|
||||||
./gradlew testDebugUnitTest # 87 tests (44 sans les données locales)
|
./gradlew testDebugUnitTest # 90 tests (72 sans les données locales)
|
||||||
|
./gradlew lint # lint vert obligatoire avant release
|
||||||
```
|
```
|
||||||
|
|
||||||
Installation sur un téléphone : mode développeur + Débogage USB, puis Android Studio
|
Installation sur un téléphone : mode développeur + Débogage USB, puis Android Studio
|
||||||
@ -84,7 +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) —
|
Dépôts : **gitea.cloudyfy.fr** et **gitea.farewell.dev** (miroir) —
|
||||||
`Siphonight/HormoneTrack` sur les deux (privé), avec
|
`Siphonight/HormoneTrack` sur les deux (privé), avec
|
||||||
**releases taguées** (`v1.1.0` → `v1.3.3`) et **deux APK par release** (depuis v1.2.5)
|
**releases taguées** (`v1.1.0` → `v1.3.4`) et **deux APK par release** (depuis v1.2.5)
|
||||||
(téléchargeables sans compiler, cf [docs/DEVELOPPEMENT.md §16.bis](docs/DEVELOPPEMENT.md)) :
|
(téléchargeables sans compiler, cf [docs/DEVELOPPEMENT.md §16.bis](docs/DEVELOPPEMENT.md)) :
|
||||||
`-release.apk` (**recommandé**, optimisé R8, 2,4 Mo) et `-debug.apk` (20 Mo) :
|
`-release.apk` (**recommandé**, optimisé R8, 2,4 Mo) et `-debug.apk` (20 Mo) :
|
||||||
|
|
||||||
@ -140,7 +141,9 @@ HormoneTrack/
|
|||||||
│ ├── CHANGELOG.md journal détaillé des versions
|
│ ├── CHANGELOG.md journal détaillé des versions
|
||||||
│ └── MONTRE-GADGETBRIDGE.md montre Huawei GT 3 : options + limites
|
│ └── MONTRE-GADGETBRIDGE.md montre Huawei GT 3 : options + limites
|
||||||
├── scripts/
|
├── scripts/
|
||||||
│ └── gitea-release.py publie une release (corps = CHANGELOG + APK)
|
│ ├── gitea-release.py publie le corps d'une release (CHANGELOG + APK)
|
||||||
|
│ ├── publish-release.py publie les 2 APK d'un tag (vérification par téléchargement)
|
||||||
|
│ └── seed-emulator.py injecte un backup JSON dans la DB d'un émulateur (cf §16.ter)
|
||||||
├── local-test-data/ ← gitignoré : backups réels pour les tests
|
├── local-test-data/ ← gitignoré : backups réels pour les tests
|
||||||
│ de régression (JAMAIS dans le dépôt, cf §8.bis)
|
│ de régression (JAMAIS dans le dépôt, cf §8.bis)
|
||||||
├── build.gradle.kts config Gradle racine (AGP/Kotlin/KSP épinglés)
|
├── build.gradle.kts config Gradle racine (AGP/Kotlin/KSP épinglés)
|
||||||
|
|||||||
@ -17,8 +17,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 = 17
|
versionCode = 18
|
||||||
versionName = "1.3.3"
|
versionName = "1.3.4"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
@ -61,6 +61,15 @@ android {
|
|||||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lint (v1.3.4) : les checks Compose 1.12+ (NonObservableLocale,
|
||||||
|
// LocalContextGetResourceValueCall) sont rétrogradés en WARNING via
|
||||||
|
// app/lint.xml (meilleure compatibilité que la DSL AGP) — cas THÉORIQUES de staleness de
|
||||||
|
// configuration présents dans tout le code v1, jamais à l'origine des
|
||||||
|
// crashs remontés. Les vrais problèmes de format de strings
|
||||||
|
// (StringFormatMatches — aurait attrapé les bugs #47/#48, épinglés §14)
|
||||||
|
// restent des ERREURS bloquantes. Re-traiter ces warnings lors d'une
|
||||||
|
// refonte i18n (cf §20).
|
||||||
}
|
}
|
||||||
|
|
||||||
// Synchronise docs/CHANGELOG.md → src/main/assets/changelog.md (dialog
|
// Synchronise docs/CHANGELOG.md → src/main/assets/changelog.md (dialog
|
||||||
|
|||||||
20
app/lint.xml
Normal file
20
app/lint.xml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
Lint (/v1.3.4). Cf commentaires dans app/build.gradle.kts et §19 de la doc de dev.
|
||||||
|
|
||||||
|
Rétrogradation des deux checks Compose 1.12+ : signalements de staleness
|
||||||
|
de configuration (lecture de resources via LocalContext dans les callbacks
|
||||||
|
et coroutines, Locale.getDefault() dans un remember{}) — présents dans tout
|
||||||
|
le code v1, jamais à l'origine des crashs remontés. À re-traiter lors d'une
|
||||||
|
refonte i18n (cf docs/DEVELOPPEMENT.md §20).
|
||||||
|
|
||||||
|
RESTENT EN ERREUR (bloquants, aucun override nécessaire) :
|
||||||
|
- StringFormatMatches / StringFormatCount / StringFormatInvalid :
|
||||||
|
auraient attrapé les bugs #47 (hrt_duration : 3 placeholders vs 2 args
|
||||||
|
passés) et #48 (pattern horaire formaté sur LocalDate) — crashs réels
|
||||||
|
v1.3.1→v1.3.3 retrouvé au téléphone, cf §14.
|
||||||
|
-->
|
||||||
|
<lint>
|
||||||
|
<issue id="NonObservableLocale" severity="warning" />
|
||||||
|
<issue id="LocalContextGetResourceValueCall" severity="warning" />
|
||||||
|
</lint>
|
||||||
@ -182,8 +182,11 @@ class ReminderReceiver : BroadcastReceiver() {
|
|||||||
} else {
|
} else {
|
||||||
context.getString(R.string.reminder_title_plain)
|
context.getString(R.string.reminder_title_plain)
|
||||||
}
|
}
|
||||||
|
// ⚠️ reminder_text_with_dose attend un %s : passer le Double brut
|
||||||
|
// déclenche un warning lint StringFormatMatches (« suspicious type ») —
|
||||||
|
// formater explicitement en dose humaine (ex. « 4,0 »).
|
||||||
val text = if (dose != null && dose > 0.0) {
|
val text = if (dose != null && dose > 0.0) {
|
||||||
context.getString(R.string.reminder_text_with_dose, dose)
|
context.getString(R.string.reminder_text_with_dose, dose.toString())
|
||||||
} else {
|
} else {
|
||||||
context.getString(R.string.reminder_text)
|
context.getString(R.string.reminder_text)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -122,8 +122,13 @@ fun DosesScreen() {
|
|||||||
val totalDays = HrtDuration.daysSince(first, System.currentTimeMillis())
|
val totalDays = HrtDuration.daysSince(first, System.currentTimeMillis())
|
||||||
if (totalDays > 0) {
|
if (totalDays > 0) {
|
||||||
val (months, days) = HrtDuration.monthsAndDays(totalDays)
|
val (months, days) = HrtDuration.monthsAndDays(totalDays)
|
||||||
|
// ⚠️ hrt_duration a TROIS placeholders (%1$d mois, %2$d jours,
|
||||||
|
// %3$d total) : passer aussi totalDays, sinon
|
||||||
|
// MissingFormatArgumentException = crash de l'écran Doses
|
||||||
|
// dès que totalDays > 0 (donc à chaque affichage avec des
|
||||||
|
// données réelles). Testé au bug réel v1.3.3 (cf §14 #47).
|
||||||
Text(
|
Text(
|
||||||
if (months > 0) stringResource(R.string.hrt_duration, months, days)
|
if (months > 0) stringResource(R.string.hrt_duration, months, days, totalDays)
|
||||||
else stringResource(R.string.hrt_duration_days_only, days),
|
else stringResource(R.string.hrt_duration_days_only, days),
|
||||||
style = MaterialTheme.typography.bodyLarge
|
style = MaterialTheme.typography.bodyLarge
|
||||||
)
|
)
|
||||||
|
|||||||
@ -52,12 +52,11 @@ import com.hormonetrack.pk.PharmacokineticEngine
|
|||||||
import com.hormonetrack.pk.TConfig
|
import com.hormonetrack.pk.TConfig
|
||||||
import com.hormonetrack.reminder.AlarmScheduler
|
import com.hormonetrack.reminder.AlarmScheduler
|
||||||
import com.hormonetrack.ui.LocalAppContainer
|
import com.hormonetrack.ui.LocalAppContainer
|
||||||
|
import com.hormonetrack.util.ExportFileNames
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.time.LocalDate
|
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@ -362,8 +361,7 @@ fun SettingsScreen(onBack: () -> Unit) {
|
|||||||
Spacer(Modifier.height(6.dp))
|
Spacer(Modifier.height(6.dp))
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
Button(onClick = {
|
Button(onClick = {
|
||||||
val date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"))
|
exportLauncher.launch(ExportFileNames.backupFileName())
|
||||||
exportLauncher.launch("hormonetrack-backup-$date.json")
|
|
||||||
}) { Text(stringResource(R.string.export_json)) }
|
}) { Text(stringResource(R.string.export_json)) }
|
||||||
OutlinedButton(onClick = {
|
OutlinedButton(onClick = {
|
||||||
importLauncher.launch(arrayOf("application/json"))
|
importLauncher.launch(arrayOf("application/json"))
|
||||||
@ -385,8 +383,10 @@ fun SettingsScreen(onBack: () -> Unit) {
|
|||||||
Spacer(Modifier.height(6.dp))
|
Spacer(Modifier.height(6.dp))
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
OutlinedButton(onClick = {
|
OutlinedButton(onClick = {
|
||||||
val date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmm"))
|
// Nom de fichier via ExportFileNames (cf §14 #48) :
|
||||||
logExportLauncher.launch("hormonetrack-logs-$date.txt")
|
// extrait et testé — passer un LocalDate au pattern
|
||||||
|
// horaire crashait l'app au tap (bug v1.3.1→v1.3.3).
|
||||||
|
logExportLauncher.launch(ExportFileNames.diagnosticLogFileName())
|
||||||
}) { Text(stringResource(R.string.logs_export)) }
|
}) { Text(stringResource(R.string.logs_export)) }
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
AppLog.clear()
|
AppLog.clear()
|
||||||
|
|||||||
46
app/src/main/java/com/hormonetrack/util/ExportFileNames.kt
Normal file
46
app/src/main/java/com/hormonetrack/util/ExportFileNames.kt
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package com.hormonetrack.util
|
||||||
|
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Noms de fichiers d'export (horodatés) — helper PUR, testable en JVM.
|
||||||
|
*
|
||||||
|
* Pourquoi cet objet existe (v1.3.4, cf §14 #48) : le nom du fichier des logs
|
||||||
|
* était généré INLINE dans le handler du bouton via
|
||||||
|
* `LocalDate.now().format(ofPattern("yyyyMMdd-HHmm"))`. Un `LocalDate` n'a
|
||||||
|
* PAS de champ horaire : le pattern `HHmm` levait
|
||||||
|
* `UnsupportedTemporalTypeException: Unsupported field: HourOfDay` de façon
|
||||||
|
* SYNCHRONE dans le onClick (thread UI) → crash de l'app au tap, sans même
|
||||||
|
* ouvrir le sélecteur de fichiers. Crash présent depuis v1.3.1 ; les « fixes »
|
||||||
|
* v1.3.2 et v1.3.3 réparaient l'IO mais pas cette ligne.
|
||||||
|
*
|
||||||
|
* Règle anti-régression : les helpers d'ici centralisent le couplage
|
||||||
|
* type ↔ pattern, chaque fonction documentant quel type porte le pattern.
|
||||||
|
* ⚠️ Règle générale : un pattern contenant des champs horaires (HH, mm, ss)
|
||||||
|
* exige LocalDateTime/LocalTime/Instant.withZone ; yyyyMMdd seul sur LocalDate.
|
||||||
|
*/
|
||||||
|
object ExportFileNames {
|
||||||
|
|
||||||
|
private val LOG_FMT = DateTimeFormatter.ofPattern("yyyyMMdd-HHmm")
|
||||||
|
private val BACKUP_FMT = DateTimeFormatter.ofPattern("yyyyMMdd")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nom du fichier d'export des logs de diagnostic :
|
||||||
|
* `hormonetrack-logs-20260906-1234.txt`.
|
||||||
|
* Le pattern contient HHmm → il faut un type TEMPOREL COMPLET
|
||||||
|
* (LocalDateTime). Passer un LocalDate ici = UnsupportedTemporalTypeException
|
||||||
|
* au tap sur « Exporter » (bug #48, crash réel v1.3.1→v1.3.3).
|
||||||
|
*/
|
||||||
|
fun diagnosticLogFileName(now: LocalDateTime = LocalDateTime.now()): String =
|
||||||
|
LOG_FMT.format(now).let { "hormonetrack-logs-$it.txt" }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nom du fichier de sauvegarde JSON :
|
||||||
|
* `hormonetrack-backup-20260906.json`. Pattern SANS heure →
|
||||||
|
* `LocalDate` est correct ici (c'est le cas qui marchait, à garder tel quel).
|
||||||
|
*/
|
||||||
|
fun backupFileName(today: LocalDate = LocalDate.now()): String =
|
||||||
|
"hormonetrack-backup-${BACKUP_FMT.format(today)}.json"
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
package com.hormonetrack.util
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Régression du bug #48 (v1.3.4) : le nom de fichier des logs était formatté
|
||||||
|
* `LocalDate.now().format(ofPattern("yyyyMMdd-HHmm"))` → TypeError
|
||||||
|
* UnsupportedTemporalTypeException: Unsupported field: HourOfDay
|
||||||
|
* levée SYNCHRONEMENT dans le onClick du bouton « Exporter » des logs
|
||||||
|
* (thread UI) → crash de l'app au tap, avant même le sélecteur de fichiers.
|
||||||
|
* Préalablement invisible en tests JVM : la logique vivait inline dans la
|
||||||
|
* SettingsScreen (composable). Le helper est désormais pur et testé ici.
|
||||||
|
*/
|
||||||
|
class ExportFileNamesTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `log filename is built from a LocalDateTime (has a time field)`() {
|
||||||
|
val name = ExportFileNames.diagnosticLogFileName(
|
||||||
|
LocalDateTime.of(2026, 9, 6, 12, 34)
|
||||||
|
)
|
||||||
|
assertEquals("hormonetrack-logs-20260906-1234.txt", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `backup filename is date-only (LocalDate is valid for yyyyMMdd)`() {
|
||||||
|
val name = ExportFileNames.backupFileName(
|
||||||
|
LocalDate.of(2026, 9, 6)
|
||||||
|
)
|
||||||
|
assertEquals("hormonetrack-backup-20260906.json", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `default filenames never throw and match the expected shape`() {
|
||||||
|
assertTrue(
|
||||||
|
ExportFileNames.diagnosticLogFileName()
|
||||||
|
.matches(Regex("hormonetrack-logs-\\d{8}-\\d{4}\\.txt"))
|
||||||
|
)
|
||||||
|
assertTrue(
|
||||||
|
ExportFileNames.backupFileName()
|
||||||
|
.matches(Regex("hormonetrack-backup-\\d{8}\\.json"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,51 @@
|
|||||||
# Changelog — HormoneTrack
|
# Changelog — HormoneTrack
|
||||||
|
|
||||||
Format : [Keep a Changelog](https://keepachangelog.com/fr-FR/1.1.0/).
|
Format : [Keep a Changelog](https://keepachangelog.com/fr-FR/1.1.0/).
|
||||||
Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.3.3`, …).
|
Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.3.4`, …).
|
||||||
|
|
||||||
|
## [1.3.4] — 2026-09-06 (versionCode 18)
|
||||||
|
|
||||||
|
### Corrigé
|
||||||
|
- **L'écran Doses faisait crasher l'app** (remontée) : `MissingFormatArgumentException:
|
||||||
|
Format specifier '%3$d'` — la string `hrt_duration` a TROIS placeholders
|
||||||
|
(`%1$d mois, %2$d jours, %3$d total`) mais l'appel transmettait 2 arguments.
|
||||||
|
Crash dès le premier affichage de l'écran Doses avec une donnée antérieure à
|
||||||
|
aujourd'hui. **Le bug est né en v1.3.1** (string + appel dans le même commit,
|
||||||
|
jamais testés ensemble) et a survécu aux releases v1.3.1→v1.3.3. Fix : passer
|
||||||
|
totalDays en 3ᵉ argument (cf §14 #47).
|
||||||
|
- **L'export des logs de diagnostic faisait crasher l'app** (remontée répétée ;
|
||||||
|
les « fixes » IO de v1.3.2 et v1.3.3 étaient corrects mais à côté du vrai
|
||||||
|
problème) : le nom de fichier était construit via
|
||||||
|
`LocalDate.now().format(ofPattern("yyyyMMdd-HHmm"))` — un `LocalDate` n'a pas
|
||||||
|
de champ horaire → `UnsupportedTemporalTypeException: Unsupported field:
|
||||||
|
HourOfDay`, levée SYNCHRONEMENT dans le onClick (thread UI) au tap sur
|
||||||
|
« Exporter », avant même l'ouverture du gestionnaire de fichiers. L'export
|
||||||
|
JSON fonctionnait car son pattern `yyyyMMdd` est valide pour un LocalDate.
|
||||||
|
FIX : utilitaire `ExportFileNames` (pur, testé) — `diagnosticLogFileName()`
|
||||||
|
utilise un `LocalDateTime` ; `backupFileName()` reste sur `LocalDate`.
|
||||||
|
Cf §14 #48.
|
||||||
|
- **Lint `StringFormatMatches`** (détecté en implantant le lint — nouveau filet
|
||||||
|
de la v1.3.4) : la notification de rappel passait un Double à une `%s`.
|
||||||
|
Fix : `dose.toString()` (comportement d'affichage inchangé, #49).
|
||||||
|
|
||||||
|
### Ajouté
|
||||||
|
- **Lint en filet bloquant** : `./gradlew lint` est désormais vert (it aurait
|
||||||
|
attrapé dès v1.3.1 les deux crashs ci-dessus via `StringFormatMatches`).
|
||||||
|
Les deux checks Compose 1.12+ théoriques (`NonObservableLocale`,
|
||||||
|
`LocalContextGetResourceValueCall`) sont rétrogradés en warning via
|
||||||
|
`app/lint.xml` (staleness de config, refonte i18n plus tard, cf §19/§20).
|
||||||
|
- **Recette reproductible : test manuel sur émulateur** (documentée §16.ter) :
|
||||||
|
émulateur + image ATD ou google_apis, seed de la DB du debug avec un backup
|
||||||
|
JSON via le nouveau script **`scripts/seed-emulator.py`** (générique, sans
|
||||||
|
aucune donnée embarquée), installation du APK release par-dessus (données
|
||||||
|
conservées, même signature), pilotage par `uiautomator dump` + `input tap`,
|
||||||
|
crashs via `adb logcat -b crash`. C'est cette recette qui a reproduit et
|
||||||
|
confirmé les deux crashs ci-dessus ainsi que LEUR correction (flux SAF
|
||||||
|
complet : picker → SAVE → fichier écrit → message de confirmation, sur
|
||||||
|
l'APK release données réelles).
|
||||||
|
- **3 nouveaux tests** (`ExportFileNamesTest`, 90 au total / 72 sans les
|
||||||
|
données locales) : épinglent le format des noms de fichiers d'export —
|
||||||
|
auraient attrapé le bug #48 le jour même.
|
||||||
|
|
||||||
## [1.3.3] — 2026-09-06 (versionCode 17)
|
## [1.3.3] — 2026-09-06 (versionCode 17)
|
||||||
|
|
||||||
|
|||||||
@ -80,6 +80,7 @@ Fonctionnalités v1 :
|
|||||||
| 6 sept. 2026 (session v1.3.2) | 3 bugs remontés : toggle agenda (callback async fix + ContextCompat au save + Posologie requise), export logs plantait (pattern JSON réutilisé + AppLog), dialog changelog récurrent (version vue mémorisée avant affichage + titre BuildConfig documenté) ; export réel mis à jour (3 traitements : EV inactif + EEn actif + CPA oral) ; **87 tests verts**, APK v1.3.2 + releases. |
|
| 6 sept. 2026 (session v1.3.2) | 3 bugs remontés : toggle agenda (callback async fix + ContextCompat au save + Posologie requise), export logs plantait (pattern JSON réutilisé + AppLog), dialog changelog récurrent (version vue mémorisée avant affichage + titre BuildConfig documenté) ; export réel mis à jour (3 traitements : EV inactif + EEn actif + CPA oral) ; **87 tests verts**, APK v1.3.2 + releases. |
|
||||||
| 6 sept. 2026 (session farewell) | Repo créé côté farewell → **push SSH (alias `farewell` : giteassh:2222) + 12 releases publiées avec APK vérifiés par téléchargement** ; piège lexicographique `v1.2.10 < v1.2.5` en comparaison de chaînes épinglé (§14 #38) ; les deux instances Gitea sont synchrones. |
|
| 6 sept. 2026 (session farewell) | Repo créé côté farewell → **push SSH (alias `farewell` : giteassh:2222) + 12 releases publiées avec APK vérifiés par téléchargement** ; piège lexicographique `v1.2.10 < v1.2.5` en comparaison de chaînes épinglé (§14 #38) ; les deux instances Gitea sont synchrones. |
|
||||||
| 6 sept. 2026 (session v1.3.3) | **Audit de reprise de maintenance** (nouvelle session IA) : 3 bugs racines trouvés — permissions agenda ABSENTES du manifest (jamais déclarées, §14 #44), export logs plantait TOUJOURS (le fix v1.3.2 réimplémentait l'IO au lieu de réutiliser BackupManager, §14 #45), bump de version JAMAIS commité (tags v1.3.0–1.3.2 tous `versionCode 14 / "1.3.0"`, §14 #46). Corrigés + doc rafraîchie (DB v3, targetSdk 36, régression 3 = export v1.3.1, §19/§20 staleness) ; **téléphone de test documenté : Google Pixel 9 /e/OS** (AOSP ; SAF DocumentsUI standard). |
|
| 6 sept. 2026 (session v1.3.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.0) | Dialog « Nouveautés » après mise à jour (CHANGELOG embarqué en asset, tâche `copyChangelog`, version vue en DataStore) ; **événements d'agenda récurrents** (calendrier local HormoneTrack, RRULE posologie, permission runtime, Room v3 `calendarEventId`) ; Paramètres : version + lien releases ; **78 tests verts**, APK v1.3.0 + releases. |
|
| 6 sept. 2026 (session v1.3.0) | Dialog « Nouveautés » après mise à jour (CHANGELOG embarqué en asset, tâche `copyChangelog`, version vue en DataStore) ; **événements d'agenda récurrents** (calendrier local HormoneTrack, RRULE posologie, permission runtime, Room v3 `calendarEventId`) ; Paramètres : version + lien releases ; **78 tests verts**, APK v1.3.0 + releases. |
|
||||||
| 5 sept. 2026 (session v1.2.10) | Sens des boutons de zoom inversé (+ = zoom avant, convention carte — retour utilisateur) ; 67 tests verts, APK v1.2.10 + releases. |
|
| 5 sept. 2026 (session v1.2.10) | Sens des boutons de zoom inversé (+ = zoom avant, convention carte — retour utilisateur) ; 67 tests verts, APK v1.2.10 + releases. |
|
||||||
| 5 sept. 2026 (session v1.2.9) | **Zoom du graphique** (pinch + boutons, 6 h → 300 j, focal stable, échantillonnage adaptatif `stepForRange`, labels X 1 h/3 h) ; **README : disclaimer IA-assisté** en en-tête ; 3 tests ; **67 tests verts**, APK v1.2.9 + releases. |
|
| 5 sept. 2026 (session v1.2.9) | **Zoom du graphique** (pinch + boutons, 6 h → 300 j, focal stable, échantillonnage adaptatif `stepForRange`, labels X 1 h/3 h) ; **README : disclaimer IA-assisté** en en-tête ; 3 tests ; **67 tests verts**, APK v1.2.9 + releases. |
|
||||||
@ -136,6 +137,15 @@ Notes importantes (v1.2.0) :
|
|||||||
par `adb install` ou APK direct. Les retours utilisateur (bugs, exports JSON) viennent
|
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,
|
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.)
|
cf §17 — ce n'est PAS le téléphone.)
|
||||||
|
- **Émulateur local (installé en v1.3.4)** : SDK brew =
|
||||||
|
`/opt/homebrew/share/android-commandlinetools`. Packages installés :
|
||||||
|
`emulator` 37.1.11 + `system-images;android-31;aosp_atd;arm64-v8a` (ATD : léger,
|
||||||
|
boot rapide, MAIS DocumentsUI STUBBÉ (`fakesystemapp`) → SAF non pilotable) et
|
||||||
|
`system-images;android-36;google_apis;arm64-v8a` (image complète — REQUISE pour
|
||||||
|
tester les flux SAF). AVD `hrt` (ATD 31) et `hrt36` (API 36) créés via
|
||||||
|
avdmanager (erreur béninie « devices.xml » au create : l'AVD est créé quand même).
|
||||||
|
Boot headless : `emulator -avd hrt36 -no-window -no-audio -no-boot-anim -no-snapshot
|
||||||
|
-gpu swiftshader_indirect`. Recette de test → **§16.ter**.
|
||||||
- **SDK Android** : installé via `brew install --cask android-commandlinetools`
|
- **SDK Android** : installé via `brew install --cask android-commandlinetools`
|
||||||
→ `/opt/homebrew/share/android-commandlinetools` (524 MB)
|
→ `/opt/homebrew/share/android-commandlinetools` (524 MB)
|
||||||
- licences acceptées : `yes | sdkmanager --licenses`
|
- licences acceptées : `yes | sdkmanager --licenses`
|
||||||
@ -361,8 +371,9 @@ Type de retour : `LevelPoint(timestamp, e2, t)`.
|
|||||||
|
|
||||||
## 8. Tests unitaires
|
## 8. Tests unitaires
|
||||||
|
|
||||||
**87 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — 44 sans les
|
**87 tests JVM, tous verts** → **90 depuis v1.3.4** (`./gradlew testDebugUnitTest`) —
|
||||||
données de test locales (cf §8.bis). Dépendance : JUnit 4.13.2.
|
**72 sans les données de test locales** (cf §8.bis : les 3 classes de régression,
|
||||||
|
6 tests chacune, sont skippées via `Assume`). Dépendance : JUnit 4.13.2.
|
||||||
Emplacement : `app/src/test/java/com/hormonetrack/`. Répertoire de travail d'exécution =
|
Emplacement : `app/src/test/java/com/hormonetrack/`. Répertoire de travail d'exécution =
|
||||||
`app/` → l'asset est lu via `src/main/assets/pk_profiles.json` (fallback `app/src/…`).
|
`app/` → l'asset est lu via `src/main/assets/pk_profiles.json` (fallback `app/src/…`).
|
||||||
|
|
||||||
@ -837,6 +848,37 @@ aucun contexte, dans l'esprit de cette doc) :**
|
|||||||
AVANT le tag ; le bump fait partie du commit de release (jamais un
|
AVANT le tag ; le bump fait partie du commit de release (jamais un
|
||||||
état local non commité au moment du build).
|
état local non commité au moment du build).
|
||||||
|
|
||||||
|
**Session v1.3.4 (deux crashs remontés → reproduits sur émulateur) :**
|
||||||
|
|
||||||
|
47. **L'écran Doses crashait l'app** (`MissingFormatArgumentException:
|
||||||
|
Format specifier '%3$d'`) : la string `hrt_duration` a TROIS placeholders
|
||||||
|
(`%1$d mois, %2$d jours, %3$d total`) mais
|
||||||
|
`stringResource(R.string.hrt_duration, months, days)` ne passait que DEUX
|
||||||
|
arguments. Crash dès que le fragment `totalDays > 0` s'affiche (donc pour
|
||||||
|
TOUTE donnée antérieure à aujourd'hui) — né en v1.3.1 (string + appel dans
|
||||||
|
le même commit fa5d2df, jamais testés ensemble) et passé à travers
|
||||||
|
v1.3.1→v1.3.3.
|
||||||
|
48. **Le bouton « Exporter » des logs crashait l'app** — LE bug récurrent des
|
||||||
|
v1.3.1→v1.3.3 : le nom de fichier était construit INLINE via
|
||||||
|
`LocalDate.now().format(ofPattern("yyyyMMdd-HHmm"))`. Un `LocalDate` n'a
|
||||||
|
PAS de champ horaire → `UnsupportedTemporalTypeException:
|
||||||
|
Unsupported field: HourOfDay` levée dans le onClick (thread principal,
|
||||||
|
synchrone, touch dispatch) → crash au tap, avant même le sélecteur SAF
|
||||||
|
(stack obtenue sur émulateur : LTSI + frames R8 obfusquées). L'export
|
||||||
|
JSON marchait car `yyyyMMdd` est un pattern valide pour LocalDate.
|
||||||
|
Zones d'ombre emboîtées : v1.3.2 et v1.3.3 « corrigent » l'IO (cf #41/#45)
|
||||||
|
sans voir cette ligne. FIX : `util/ExportFileNames.kt` (pur, testé,
|
||||||
|
documenté — couplage type↔pattern centralisé) ; `FileNamesTest` épinglant
|
||||||
|
les deux helpers (aurait attrapé le bug le jour même). Leçon : la
|
||||||
|
génération de nom de fichier ne doit JAMAIS vivre inline dans un onClick.
|
||||||
|
49. **StringFormatMatches** (capture du lint, v1.3.4) : la notification de
|
||||||
|
rappel passait un Double à `%s`. Fix `dose.toString()` (l'affichage ne
|
||||||
|
change pas). L'intérêt des règles lint est confirmé PAR CETTE SESSION :
|
||||||
|
`./gradlew lint` aurait signalé #47 dès v1.3.1 → le lint fait désormais
|
||||||
|
partie de la vérification avant release (les checks Compose 1.12+
|
||||||
|
théoriques `NonObservableLocale`/`LocalContextGetResourceValueCall` sont
|
||||||
|
tombés en warning via `app/lint.xml`, cf §19).
|
||||||
|
|
||||||
**Leçons** : (a) ne jamais croire un build « probablement bon » sans l'avoir lancé ;
|
**Leçons** : (a) ne jamais croire un build « probablement bon » sans l'avoir lancé ;
|
||||||
(b) les tests sémantiques attrapent ce que la compilation ne voit pas ; (c) se méfier des
|
(b) les tests sémantiques attrapent ce que la compilation ne voit pas ; (c) se méfier des
|
||||||
constantes stdlib « de mémoire » (`ln2`), des mélanges Float/Double, et des APIs M3
|
constantes stdlib « de mémoire » (`ln2`), des mélanges Float/Double, et des APIs M3
|
||||||
@ -852,10 +894,15 @@ perd les garde-fous acquis à l'usage (cf #45) ; (j) vérifier les POSTULATS dan
|
|||||||
code réel, pas dans la doc (« le manifest contient WRITE_CALENDAR », cf #44) ;
|
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
|
(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
|
(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
|
confidentialité s'ADAPTE automatiquement aux nouveaux exports
|
||||||
donc à CHECK chaque push — un historique « propre hier » peut devenir hit dès
|
(`local-test-data/`) : un historique « propre hier » peut devenir hit dès
|
||||||
qu'un nouvel export introduit des motifs qui collent ; juger les hits avec
|
qu'un nouvel export introduit des motifs qui collent — re-scanner PUIS juger
|
||||||
l'utilisatrice (cf dette documentée §8.bis).
|
avec l'utilisatrice (dette déjà jugée, cf §8.bis) ; (m) **un crash sans stack
|
||||||
|
trace = d'abord le reproduire** (émulateur + données réelles, §16.ter) — les
|
||||||
|
deux « fixes » aveugles de v1.3.2/v1.3.3 (#41/#45) ont laissé passer un bug
|
||||||
|
trivial (#48) seulement visible sur l'émulateur ; (n) `./gradlew lint` fait
|
||||||
|
désormais partie de la vérification avant release : `StringFormatMatches`
|
||||||
|
aurait signalé #47 dès v1.3.1.
|
||||||
|
|
||||||
## 15. Comment régénérer l'asset pk_profiles.json
|
## 15. Comment régénérer l'asset pk_profiles.json
|
||||||
|
|
||||||
@ -980,8 +1027,11 @@ vérifie pas en tests JVM).
|
|||||||
|
|
||||||
1. **Bumper la version** dans `app/build.gradle.kts` : `versionCode = N+1`,
|
1. **Bumper la version** dans `app/build.gradle.kts` : `versionCode = N+1`,
|
||||||
`versionName = "X.Y.Z+1"` (SemVer : fix = Z, feature = Y).
|
`versionName = "X.Y.Z+1"` (SemVer : fix = Z, feature = Y).
|
||||||
2. **Tests verts obligatoires** : `./gradlew testDebugUnitTest` — 87 au total,
|
2. **Tests verts obligatoires** : `./gradlew testDebugUnitTest` — 90 au total
|
||||||
44 si `local-test-data/` est absent (les régressions réelles sont skippées).
|
(v1.3.4), 72 si `local-test-data/` est absent (les 3 classes de régression
|
||||||
|
réelles sont skippées via `Assume`, 6 tests chacune) ; **lint vert
|
||||||
|
obligatoire** : `./gradlew lint` (v1.3.4 — `StringFormatMatches` aurait
|
||||||
|
attrapé les crashs #47/#48 dès v1.3.1).
|
||||||
3. **Docs** : section `## [X.Y.Z]` en tête de `docs/CHANGELOG.md` (le corps des
|
3. **Docs** : section `## [X.Y.Z]` en tête de `docs/CHANGELOG.md` (le corps des
|
||||||
releases Gitea en sera extrait automatiquement par le script), + §14 si bug
|
releases Gitea en sera extrait automatiquement par le script), + §14 si bug
|
||||||
corrigé, + §2 (historique) si notable.
|
corrigé, + §2 (historique) si notable.
|
||||||
@ -1012,10 +1062,83 @@ Notes :
|
|||||||
de santé dans le dépôt, cf §8.bis).
|
de santé dans le dépôt, cf §8.bis).
|
||||||
|
|
||||||
- Téléphone : mode développeur + Débogage USB (détails : GUIDE_INSTALLATION.md)
|
- Téléphone : mode développeur + Débogage USB (détails : GUIDE_INSTALLATION.md)
|
||||||
- À ma charge (assistant) : build + tests JVM ✓ ; émulateur possible sur demande ;
|
- À ma charge (assistant) : build + tests JVM ✓ ; **émulateur installé et
|
||||||
|
opérationnel depuis v1.3.4 (recette §16.ter)** ;
|
||||||
**les tests humains sur vrai téléphone restent la référence**
|
**les tests humains sur vrai téléphone restent la référence**
|
||||||
(notifs → montre, UX de saisie, pickers, panoramique du chart)
|
(notifs → montre, UX de saisie, pickers, panoramique du chart)
|
||||||
|
|
||||||
|
### 16.ter Recette : test manuel sur émulateur (reproductible — v1.3.4)
|
||||||
|
|
||||||
|
But : reproduire/valider un crash ou une UI sur l'app RÉELLE (sans téléphone
|
||||||
|
branché) — c'est cette recette qui a diagnostiqué les crashs #47/#48 en une
|
||||||
|
session. ⚠️ AUCUNE donnée personnelle n'est embarquée : le backup utilisé vit
|
||||||
|
sous `local-test-data/` (gitignoré, §8.bis) et est passé au script en argument.
|
||||||
|
|
||||||
|
**0. Installation (one-shot, cf §4)** : `sdkmanager "emulator"
|
||||||
|
"system-images;android-31;aosp_atd;arm64-v8a"` (léger, pour le LOGIQUE sans
|
||||||
|
SAF) et/ou `"system-images;android-36;google_apis;arm64-v8a"` (image COMPLÈTE,
|
||||||
|
nécessaire pour piloter les sélecteurs de fichiers). AVD :
|
||||||
|
`avdmanager create avd -n hrt36 -k "system-images;…" -d pixel_6` (l'erreur
|
||||||
|
béninie « devices.xml » ne bloque pas la création). Boot headless :
|
||||||
|
`emulator -avd hrt36 -no-window -no-audio -no-boot-anim -no-snapshot
|
||||||
|
-gpu swiftshader_indirect &` puis `adb wait-for-device` + attente
|
||||||
|
`getprop sys.boot_completed`.
|
||||||
|
|
||||||
|
**1. Seed des « données réelles »** — la DB est modifiable uniquement sur un
|
||||||
|
APK DEBUGGABLE (`run-as`) :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
adb install app/build/outputs/apk/debug/app-debug.apk
|
||||||
|
adb shell am start -n com.hormonetrack/.MainActivity # 1er lancement → crée la DB
|
||||||
|
adb shell am force-stop com.hormonetrack # flush de la DB
|
||||||
|
adb shell run-as com.hormonetrack cat databases/hormonetrack.db > /tmp/hrt.db
|
||||||
|
python3 scripts/seed-emulator.py /tmp/hrt.db local-test-data/backup-v1.3.1.json
|
||||||
|
adb push /tmp/hrt.db /data/local/tmp/hrt.db
|
||||||
|
adb shell run-as com.hormonetrack cp /data/local/tmp/hrt.db databases/hormonetrack.db
|
||||||
|
adb shell run-as com.hormonetrack rm -f databases/hormonetrack.db-wal \
|
||||||
|
databases/hormonetrack.db-shm # WAL périmé sinon ! (checkpoint implicite fait par le seed)
|
||||||
|
```
|
||||||
|
|
||||||
|
Règles du seed (le script les impose) : colonnes = noms de propriétés Kotlin
|
||||||
|
(Room n'applique pas de snake_case), enums stockés en String — on insère
|
||||||
|
SEULEMENT des lignes, on ne touche NI au schéma NI à `room_master_table`
|
||||||
|
(Room valide le schéma à l'ouverture, pas les lignes).
|
||||||
|
|
||||||
|
**2. Passer en RELEASE (c'est LE build à tester)** — `install -r` PAR-DESSUS
|
||||||
|
le debug CONSERVE la DB (les APK release/signés debug sont la même clé) :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
adb install -r app/build/outputs/apk/release/app-release.apk
|
||||||
|
adb logcat -c && adb shell am start -n com.hormonetrack/.MainActivity
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Piloter l'UI headless** : à l'aide de `uiautomator dump` + `input tap` —
|
||||||
|
l'UI se pilote SANS écran :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
adb shell uiautomator dump /sdcard/ui.xml && adb shell cat /sdcard/ui.xml
|
||||||
|
# → parser text=…/content-desc=… + bounds=[x1,y1][x2,y2] → centre (cx,cy)
|
||||||
|
adb shell input tap <cx> <cy> # naviguer (5 tabs ≈ y=2280 à 1080×2400)
|
||||||
|
adb shell input keyevent KEYCODE_BACK
|
||||||
|
```
|
||||||
|
|
||||||
|
**4. Attraper le crash** : le process meurt et repasse au launcher — c'est le
|
||||||
|
signal ; stack : `adb logcat -b crash -d` (grep FATAL). **0 crash attendu :
|
||||||
|
`grep -c FATAL` reste à 0** après chaque interaction.
|
||||||
|
|
||||||
|
**5. Flux SAF (export JSON/logs)** : sur android-31 ATD le
|
||||||
|
DocumentsUI est STUBBÉ (`fakesystemapp`) → INUTILISABLE ; utiliser l'image
|
||||||
|
android-36 google_apis. Le picker se pilote pareil (bouton `SAVE` en bas à
|
||||||
|
droite) ; vérification : fichier présent dans `/sdcard/Download/` + le message
|
||||||
|
de confirmation (v1.3.3 : « Logs de diagnostic exportés ») visible via un
|
||||||
|
nouveau dump UI.
|
||||||
|
|
||||||
|
**6. Vérifications post-fix** : Doses (en-tête « temps sous THS » + liste),
|
||||||
|
Settings → Export logs (SAF + SAVE + message), export/import JSON, changelog
|
||||||
|
dialog au premier lancement (sinon re-seed un `changelog_seen_version`).
|
||||||
|
⚠️ Les coordonnées UI (x/y) évoluent avec l'écran/l'app : TOUJOURS re-dumper
|
||||||
|
`uiautomator` avant de taper — ne jamais figer des coordonnées hors dump.
|
||||||
|
|
||||||
## 17. Montre : Gadgetbridge & options
|
## 17. Montre : Gadgetbridge & options
|
||||||
|
|
||||||
Doc dédiée : [MONTRE-GADGETBRIDGE.md](MONTRE-GADGETBRIDGE.md). Synthèse :
|
Doc dédiée : [MONTRE-GADGETBRIDGE.md](MONTRE-GADGETBRIDGE.md). Synthèse :
|
||||||
@ -1062,6 +1185,11 @@ Volontaires (v1) :
|
|||||||
explicites, mais R8 ne se vérifie pas en tests JVM → **smoke-test sur
|
explicites, mais R8 ne se vérifie pas en tests JVM → **smoke-test sur
|
||||||
téléphone** (export/import backup, graphiques) avant chaque publication ;
|
téléphone** (export/import backup, graphiques) avant chaque publication ;
|
||||||
signé clé debug → upgradable sans perte, mais pas une signature « officielle »
|
signé clé debug → upgradable sans perte, mais pas une signature « officielle »
|
||||||
|
- **Lint (v1.3.4)** : `./gradlew lint` est vert — `StringFormatMatches` et
|
||||||
|
famille restent des ERREURS bloquantes ; les checks Compose 1.12+
|
||||||
|
théoriques (`NonObservableLocale`, `LocalContextGetResourceValueCall`,
|
||||||
|
staleness de configuration) sont rétrogradés en WARNING via `app/lint.xml`
|
||||||
|
→ à re-traiter lors d'une refonte i18n (§20)
|
||||||
|
|
||||||
## 20. Idées d'évolution
|
## 20. Idées d'évolution
|
||||||
|
|
||||||
@ -1135,11 +1263,25 @@ Sur le téléphone de test (à compléter par l'utilisatrice) :
|
|||||||
accord, l'événement récurrent apparaît dans l'app d'agenda du Pixel 9 ;
|
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
|
le smoke-test R8 de la checklist §16 doit aussi couvrir export/import
|
||||||
de backup + graphiques (réflexion Gson sous R8)
|
de backup + graphiques (réflexion Gson sous R8)
|
||||||
|
- [ ] **v1.3.4** : **écran Doses** s'ouvre et affiche l'en-tête « temps sous
|
||||||
|
THS » avec les données réelles (bug #47 : crash dès que des doses
|
||||||
|
antérieures à aujourd'hui existaient — né v1.3.1)
|
||||||
|
- [ ] **v1.3.4** : **export des logs** ouvre VRAIMENT le gestionnaire de
|
||||||
|
fichiers et le message « Logs de diagnostic exportés » apparaît après
|
||||||
|
la sauvegarde (bug #48 : crash au tap depuis v1.3.1) — vérifié sur
|
||||||
|
émulateur API 36 (§16.ter), à confirmer au téléphone
|
||||||
|
- [ ] **v1.3.4** : rappel de notification affiche la dose proprement
|
||||||
|
(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)
|
||||||
|
|
||||||
---
|
---
|
||||||
*Doc mise à jour le 6 sept. 2026 (v1.3.3) — build OK, 87/87 tests verts (44 sans les
|
*Doc mise à jour le 6 sept. 2026 (v1.3.4) — build OK, lint vert, 90/90 tests verts (72 sans
|
||||||
données locales), dépôts Gitea (cloudyfy + farewell) avec releases APK, aucune donnée
|
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 :
|
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
|
3 bugs racines corrigés (permissions agenda, export logs, bump de version). v1.3.4 :
|
||||||
raccourcie (DB v3, targetSdk 36, régression 3 = export v1.3.1, numéros #38/#43 rétablis,
|
2 crashs remontés reproduits sur émulateur avec les données réelles (recette §16.ter,
|
||||||
staleness §19/§20 nettoyé).*
|
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
|
||||||
|
(v1.1.0→v1.2.3) — cf §8.bis.*
|
||||||
|
|||||||
@ -108,9 +108,12 @@ Les notifications de l'app remontent automatiquement sur la montre via **Huawei
|
|||||||
- Choisis bien l'unité : elle est convertie automatiquement pour l'affichage et la
|
- Choisis bien l'unité : elle est convertie automatiquement pour l'affichage et la
|
||||||
calibration (E2 en pg/mL ; T en ng/mL, ng/dL, ng/L, nmol/L)
|
calibration (E2 en pg/mL ; T en ng/mL, ng/dL, ng/L, nmol/L)
|
||||||
4. **Calibration** (dans l'édition d'un traitement E2) → « Calibrer avec les analyses »
|
4. **Calibration** (dans l'édition d'un traitement E2) → « Calibrer avec les analyses »
|
||||||
- Paramètres → **Logs de diagnostic** : exporte le journal de l'app (utile
|
- Paramètres → **Logs de diagnostic** : « Exporter » ouvre le gestionnaire de
|
||||||
en cas de bug à signaler)
|
fichiers → choisis où enregistrer le .txt → un message de confirmation
|
||||||
→ calcule le facteur d'échelle = médiane(lab ÷ prédiction), comme le « Scale factor » du `.ods`
|
(ou d'erreur) s'affiche en haut de Paramètres — utile pour joindre les
|
||||||
|
logs à un rapport de bug (≥ v1.3.4 ; les versions 1.3.1–1.3.3
|
||||||
|
faisaient planter ce bouton)
|
||||||
|
→ calcule le facteur d'échelle = médiane(lab ÷ prédiction), comme le « Scale factor » du `.ods`
|
||||||
5. **Paramètres** :
|
5. **Paramètres** :
|
||||||
- **Langue** : Système / Français / English
|
- **Langue** : Système / Français / English
|
||||||
- **Estimation T** : modèle `T = plancher + (base − plancher) ÷ (1 + k·E2)` (ng/mL),
|
- **Estimation T** : modèle `T = plancher + (base − plancher) ÷ (1 + k·E2)` (ng/mL),
|
||||||
@ -164,6 +167,7 @@ Les profils viennent de ta feuille `Estrogen.ods` (table « Models ») :
|
|||||||
| Notif absente sur la montre | Huawei Santé → Notifications → autorise l'app ; redémarre la montre |
|
| Notif absente sur la montre | Huawei Santé → Notifications → autorise l'app ; redémarre la montre |
|
||||||
| Rappels en retard | Paramètres → « Accorder les alarmes exactes » + désactive l'optimisation de batterie pour l'app |
|
| Rappels en retard | Paramètres → « Accorder les alarmes exactes » + désactive l'optimisation de batterie pour l'app |
|
||||||
| Import JSON échoué | Le fichier doit venir d'un export de l'app même version (IDs conservés) |
|
| Import JSON échoué | Le fichier doit venir d'un export de l'app même version (IDs conservés) |
|
||||||
|
| L'export des logs ou JSON a planté (versions ≤ 1.3.3) | Mettre à jour vers la v1.3.4+ (fixons confirmés sur émulateur) — le message de confirmation doit apparaître en haut de Paramètres |
|
||||||
|
|
||||||
## 10. Données & vie privée
|
## 10. Données & vie privée
|
||||||
|
|
||||||
|
|||||||
91
scripts/seed-emulator.py
Executable file
91
scripts/seed-emulator.py
Executable file
@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Seed la DB Room d'un émulateur avec un backup JSON exporté par l'app.
|
||||||
|
|
||||||
|
OUTIL DE TEST MANUEL (post-v1.3.4) — cf recette complète dans
|
||||||
|
docs/DEVELOPPEMENT.md §16.ter « Test manuel sur émulateur ».
|
||||||
|
|
||||||
|
Constitue l'étape « données réelles dans l'émulateur » : lance l'APK debug une
|
||||||
|
fois (crée la DB via Room), récupère la DB via run-as, injecte les lignes du
|
||||||
|
backup passé en argument (SANS toucher au schéma ni à room_master_table — Room
|
||||||
|
valide le schéma, pas les lignes), puis la repousse.
|
||||||
|
|
||||||
|
Usage (le téléphone = émulateur démarré, avec com.hormonetrack - APK DEBUG -
|
||||||
|
installé et lancé au moins une fois) :
|
||||||
|
|
||||||
|
adb shell am force-stop com.hormonetrack
|
||||||
|
adb shell run-as com.hormonetrack cat databases/hormonetrack.db > /tmp/hrt.db
|
||||||
|
python3 scripts/seed-emulator.py /tmp/hrt.db <backup.json> # modifie la BD locale
|
||||||
|
adb push /tmp/hrt.db /data/local/tmp/hrt.db
|
||||||
|
adb shell run-as com.hormonetrack cp /data/local/tmp/hrt.db databases/hormonetrack.db
|
||||||
|
adb shell run-as com.hormonetrack rm -f databases/hormonetrack.db-wal databases/hormonetrack.db-shm
|
||||||
|
|
||||||
|
⚠️ PRIVATITÉ : le fichier backup contient des données de santé personnelles et
|
||||||
|
vit sous `local-test-data/` (gitignoré, cf §8.bis). Ce script ne contains
|
||||||
|
AUCUNE donnée : tout passe par arguments/lignes insérées à l'exécution. Ne
|
||||||
|
JAMAIS copier un backup dans le dépôt ni en nommer dans les commentaires.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
sys.exit("usage: seed-emulator.py <db-sqlite> <backup.json>")
|
||||||
|
db_path, backup_path = sys.argv[1], sys.argv[2]
|
||||||
|
|
||||||
|
data = json.load(open(backup_path, encoding="utf-8"))
|
||||||
|
|
||||||
|
con = sqlite3.connect(db_path)
|
||||||
|
cur = con.cursor()
|
||||||
|
|
||||||
|
# ⚠️ Les colonnes portent les noms des propriétés Kotlin SANS renommage
|
||||||
|
# (Room ne convertit pas en snake_case) ; les enums sont stockés en String.
|
||||||
|
for tr in data["treatments"]:
|
||||||
|
cur.execute(
|
||||||
|
"INSERT OR REPLACE INTO treatments (id, name, type, route, doseAmount,"
|
||||||
|
" doseUnit, isActive, notes, esterType, pkModel, absorptionHours,"
|
||||||
|
" eliminationHalfLifeHours, bioavailabilityFraction, scaleFactor,"
|
||||||
|
" forecastIntervalDays, reminderHour, reminderMinute, reminderEnabled,"
|
||||||
|
" calendarEventId, createdAt)"
|
||||||
|
" VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||||
|
(
|
||||||
|
tr["id"], tr["name"], tr["type"], tr["route"], tr["doseAmount"],
|
||||||
|
tr["doseUnit"], tr["isActive"], tr.get("notes"),
|
||||||
|
tr.get("esterType", "NONE"), tr.get("pkModel", "ESE"),
|
||||||
|
tr.get("absorptionHours", 4.0),
|
||||||
|
tr.get("eliminationHalfLifeHours", 24.0),
|
||||||
|
tr.get("bioavailabilityFraction", 1.0), tr.get("scaleFactor", 1.0),
|
||||||
|
tr.get("forecastIntervalDays"), tr.get("reminderHour"),
|
||||||
|
tr.get("reminderMinute"), tr.get("reminderEnabled", False),
|
||||||
|
tr.get("calendarEventId"), tr["createdAt"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
for d in data["doseLogs"]:
|
||||||
|
cur.execute(
|
||||||
|
"INSERT OR REPLACE INTO dose_logs (id, treatmentId, timestamp,"
|
||||||
|
" doseAmount, notes, esterType) VALUES (?,?,?,?,?,?)",
|
||||||
|
(d["id"], d["treatmentId"], d["timestamp"], d["doseAmount"],
|
||||||
|
d.get("notes"), d.get("esterType")),
|
||||||
|
)
|
||||||
|
|
||||||
|
for l in data["labResults"]:
|
||||||
|
cur.execute(
|
||||||
|
"INSERT OR REPLACE INTO lab_results (id, marker, value, unit,"
|
||||||
|
" timestamp, notes) VALUES (?,?,?,?,?,?)",
|
||||||
|
(l["id"], l["marker"], l["value"], l["unit"], l["timestamp"],
|
||||||
|
l.get("notes")),
|
||||||
|
)
|
||||||
|
|
||||||
|
con.commit()
|
||||||
|
cur.execute(
|
||||||
|
"SELECT (SELECT COUNT(*) FROM treatments), (SELECT COUNT(*) FROM dose_logs),"
|
||||||
|
" (SELECT COUNT(*) FROM lab_results)")
|
||||||
|
t, d, l = cur.fetchone()
|
||||||
|
print(f"seed OK : {t} traitements, {d} doses, {l} labs")
|
||||||
|
con.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
x
Reference in New Issue
Block a user