v1.2.6 : valeurs des pics/creux sur le graphique + import JSON en mode écrasement
- CurveChart : drawExtremum = triangle ▲▼ + VALEUR estimée de l'extremum
(au-dessus d'un pic, en dessous d'un creux), couleur de la série
- Import JSON : mode ÉCRASEMENT (bug remonté : échec dès que l'app contenait
des données — conflit d'IDs backup vs existants)
· DAOs : deleteAll() sur les 3 tables (ordre enfants → parents, FK CASCADE)
· BackupManager.importJson : wipe puis restauration (IDs conservés → FK
valides), retourne aussi le tConfig du backup
· SettingsScreen : après import → tConfig restauré dans DataStore + rappels
reprogrammés (rescheduleAll) ; dialog renommé « Effacer & restaurer »
avec avertissement explicite
- docs : §13 (écrasement), §11 (valeurs extrema), §14 #34, §16.bis CHECKLIST
DE DÉPLOIEMENT complète numérotée (version → tests → docs → commit/tag →
push 2 remotes → build 2 APK → releases 2 instances → smoke-test R8),
§20/§21, CHANGELOG, README
- versionCode 9, versionName 1.2.6
This commit is contained in:
parent
fb9806afd3
commit
e16d370b2f
@ -9,7 +9,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.2.5 — build Android ✅, **62 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.2.6 — build Android ✅, **62 tests unitaires** ✅ (44 sans les données de test locales ; 3 régressions épinglées sur données réelles **non versionnées**), intégration montre = notifications ✅, **dépôt Gitea privé + releases avec APK** ✅
|
||||
- **Journal des versions** : [docs/CHANGELOG.md](docs/CHANGELOG.md)
|
||||
- **Guide utilisateur** : [docs/GUIDE_INSTALLATION.md](docs/GUIDE_INSTALLATION.md)
|
||||
- **Doc de développement** (architecture, maths, décisions, bugs) : [docs/DEVELOPPEMENT.md](docs/DEVELOPPEMENT.md)
|
||||
|
||||
@ -12,8 +12,8 @@ android {
|
||||
applicationId = "com.hormonetrack"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 8
|
||||
versionName = "1.2.5"
|
||||
versionCode = 9
|
||||
versionName = "1.2.6"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
@ -31,8 +31,26 @@ object BackupManager {
|
||||
return Gson().toJson(data)
|
||||
}
|
||||
|
||||
data class ImportResult(val treatments: Int, val doseLogs: Int, val labResults: Int)
|
||||
data class ImportResult(
|
||||
val treatments: Int,
|
||||
val doseLogs: Int,
|
||||
val labResults: Int,
|
||||
/** Réglages T du backup, à restaurer dans DataStore par l'appelant. */
|
||||
val tConfig: TConfig
|
||||
)
|
||||
|
||||
/**
|
||||
* Restaure un backup en mode **ÉCRASEMENT** (v1.2.6) : les données actuelles
|
||||
* sont effacées AVANT l'insertion — sinon les IDs du backup (conservés pour
|
||||
* les FK dose→traitement) entrent en conflit avec les IDs existants et
|
||||
* l'import échouait dès que l'app contenait déjà des données.
|
||||
*
|
||||
* Ordre d'effacement : dose_logs et lab_results d'abord, treatments ensuite
|
||||
* (FK CASCADE des doses vers les traitements).
|
||||
*
|
||||
* @return le nombre d'entrées restaurées par table + le tConfig du backup
|
||||
* (à restaurer dans DataStore par l'appelant).
|
||||
*/
|
||||
suspend fun importJson(
|
||||
repo: HormoneRepository,
|
||||
json: String
|
||||
@ -40,6 +58,11 @@ object BackupManager {
|
||||
val type = object : TypeToken<BackupData>() {}.type
|
||||
val data: BackupData = Gson().fromJson(json, type)
|
||||
|
||||
// Écrasement : on part d'une base vide (ordre : enfants → parents)
|
||||
repo.deleteAllDoseLogs()
|
||||
repo.deleteAllLabResults()
|
||||
repo.deleteAllTreatments()
|
||||
|
||||
data.treatments.forEach { repo.insertTreatment(it) }
|
||||
data.doseLogs.forEach { repo.insertDoseLog(it) }
|
||||
data.labResults.forEach { repo.insertLabResult(it) }
|
||||
@ -47,7 +70,8 @@ object BackupManager {
|
||||
return ImportResult(
|
||||
data.treatments.size,
|
||||
data.doseLogs.size,
|
||||
data.labResults.size
|
||||
data.labResults.size,
|
||||
data.tConfig
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -24,6 +24,10 @@ interface DoseLogDao {
|
||||
@Query("SELECT * FROM dose_logs ORDER BY timestamp ASC")
|
||||
suspend fun getAllOnce(): List<DoseLog>
|
||||
|
||||
/** Efface TOUT l'historique (import en mode écrasement). */
|
||||
@Query("DELETE FROM dose_logs")
|
||||
suspend fun deleteAll()
|
||||
|
||||
@Insert
|
||||
suspend fun insert(log: DoseLog): Long
|
||||
|
||||
|
||||
@ -18,6 +18,10 @@ interface LabResultDao {
|
||||
@Query("SELECT * FROM lab_results ORDER BY timestamp ASC")
|
||||
suspend fun getAllOnce(): List<LabResult>
|
||||
|
||||
/** Efface toutes les analyses (import en mode écrasement). */
|
||||
@Query("DELETE FROM lab_results")
|
||||
suspend fun deleteAll()
|
||||
|
||||
@Insert
|
||||
suspend fun insert(result: LabResult): Long
|
||||
|
||||
|
||||
@ -21,6 +21,11 @@ interface TreatmentDao {
|
||||
@Query("SELECT * FROM treatments ORDER BY createdAt DESC")
|
||||
suspend fun getAllOnce(): List<Treatment>
|
||||
|
||||
/** Efface tous les traitements (import en mode écrasement).
|
||||
* ⚠️ À appeler APRÈS deleteAll() des dose_logs (FK CASCADE sinon). */
|
||||
@Query("DELETE FROM treatments")
|
||||
suspend fun deleteAll()
|
||||
|
||||
@Insert
|
||||
suspend fun insert(treatment: Treatment): Long
|
||||
|
||||
|
||||
@ -44,4 +44,9 @@ class HormoneRepository(
|
||||
suspend fun insertLabResult(result: LabResult): Long = labResultDao.insert(result)
|
||||
suspend fun updateLabResult(result: LabResult) = labResultDao.update(result)
|
||||
suspend fun deleteLabResult(result: LabResult) = labResultDao.delete(result)
|
||||
|
||||
// --- Effacement complet (import en mode écrasement, cf BackupManager) ---
|
||||
suspend fun deleteAllDoseLogs() = doseLogDao.deleteAll()
|
||||
suspend fun deleteAllLabResults() = labResultDao.deleteAll()
|
||||
suspend fun deleteAllTreatments() = treatmentDao.deleteAll()
|
||||
}
|
||||
|
||||
@ -168,17 +168,18 @@ fun CurveChart(
|
||||
}
|
||||
}
|
||||
|
||||
// Pics et creux (v1.2.3) : triangles aux extrema locaux, couleur de la série
|
||||
// Pics et creux (v1.2.3) : triangles aux extrema locaux + valeur estimée
|
||||
// (v1.2.6 : le triangle est accompagné de la valeur simulée du pic/creux)
|
||||
if (options.showExtrema) {
|
||||
PharmacokineticEngine.detectExtrema(s.points, { it.e2 }, minSwingAbs = 2.0)
|
||||
.forEach { ex ->
|
||||
drawTriangle(xOf(ex.timestamp), yE2(ex.value), ex.isPeak, s.e2Style.color)
|
||||
drawExtremum(xOf(ex.timestamp), yE2(ex.value), ex, s.e2Style.color)
|
||||
}
|
||||
if (options.showT) {
|
||||
s.tStyle?.let { tStyle ->
|
||||
PharmacokineticEngine.detectExtrema(s.points, { it.t }, minSwingAbs = 0.02)
|
||||
.forEach { ex ->
|
||||
drawTriangle(xOf(ex.timestamp), yT(ex.value), ex.isPeak, tStyle.color)
|
||||
drawExtremum(xOf(ex.timestamp), yT(ex.value), ex, tStyle.color)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -219,24 +220,47 @@ fun CurveChart(
|
||||
}
|
||||
}
|
||||
|
||||
/** Triangle ▲ (pic, au-dessus du point) ou ▼ (creux, en dessous), couleur de la série. */
|
||||
private fun DrawScope.drawTriangle(cx: Float, cy: Float, isPeak: Boolean, color: Color) {
|
||||
/**
|
||||
* Triangle ▲ (pic, au-dessus du point) ou ▼ (creux, en dessous) + la VALEUR
|
||||
* estimée de l'extremum (v1.2.6) : valeur au-dessus d'un pic, en dessous d'un
|
||||
* creux — dans la couleur de la série, unité implicite (celle de l'axe).
|
||||
*/
|
||||
private fun DrawScope.drawExtremum(cx: Float, cy: Float, extremum: com.hormonetrack.pk.PharmacokineticEngine.Extremum, color: Color) {
|
||||
val r = 5.dp.toPx()
|
||||
val offset = 6.dp.toPx()
|
||||
val path = Path()
|
||||
if (isPeak) {
|
||||
val valueY: Float
|
||||
if (extremum.isPeak) {
|
||||
val y = cy - offset
|
||||
path.moveTo(cx, y - r)
|
||||
path.lineTo(cx - r, y + r * 0.5f)
|
||||
path.lineTo(cx + r, y + r * 0.5f)
|
||||
valueY = y - r - 3.dp.toPx()
|
||||
} else {
|
||||
val y = cy + offset
|
||||
path.moveTo(cx, y + r)
|
||||
path.lineTo(cx - r, y - r * 0.5f)
|
||||
path.lineTo(cx + r, y - r * 0.5f)
|
||||
valueY = y + r + 11.dp.toPx()
|
||||
}
|
||||
path.close()
|
||||
drawPath(path, color)
|
||||
// Valeur estimée du pic/creux, dans la couleur de la courbe
|
||||
val paint = android.graphics.Paint().apply {
|
||||
this.color = android.graphics.Color.argb(
|
||||
(color.alpha * 255).toInt(),
|
||||
(color.red * 255).toInt(),
|
||||
(color.green * 255).toInt(),
|
||||
(color.blue * 255).toInt()
|
||||
)
|
||||
textSize = 9.dp.toPx()
|
||||
textAlign = android.graphics.Paint.Align.CENTER
|
||||
isAntiAlias = true
|
||||
isFakeBoldText = true
|
||||
}
|
||||
drawContext.canvas.nativeCanvas.drawText(
|
||||
formatValue(extremum.value), cx, valueY, paint
|
||||
)
|
||||
}
|
||||
|
||||
private fun DrawScope.drawGrid(color: Color, padLeft: Float, padTop: Float, w: Float, h: Float, rows: Int) { for (i in 0..rows) {
|
||||
|
||||
@ -348,11 +348,18 @@ fun SettingsScreen(onBack: () -> Unit) {
|
||||
showImportConfirm = false
|
||||
val json = importJson ?: return@TextButton
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
// Import en mode ÉCRASEMENT : les données actuelles sont
|
||||
// effacées d'abord (BackupManager), puis le tConfig du
|
||||
// backup restaure DataStore et les rappels sont reprogrammés
|
||||
val result = try {
|
||||
BackupManager.importJson(repo, json)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
result?.let {
|
||||
container.settings.setTConfig(it.tConfig)
|
||||
AlarmScheduler(context).rescheduleAll(repo.allTreatmentsOnce())
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
message = if (result != null) {
|
||||
context.getString(R.string.import_ok, result.treatments, result.doseLogs, result.labResults)
|
||||
|
||||
@ -149,10 +149,10 @@
|
||||
<string name="export_ok">Sauvegarde exportée.</string>
|
||||
<string name="export_fail">L\'export a échoué.</string>
|
||||
<string name="import_fail">L\'import a échoué : fichier invalide.</string>
|
||||
<string name="import_ok">Importé : %1$d traitements, %2$d doses, %3$d analyses.</string>
|
||||
<string name="import_confirm">Restaurer</string>
|
||||
<string name="import_ok">Restauré : %1$d traitements, %2$d doses, %3$d analyses (les données actuelles ont été remplacées, rappels reprogrammés).</string>
|
||||
<string name="import_confirm">Effacer & restaurer</string>
|
||||
<string name="import_title">Importer une sauvegarde</string>
|
||||
<string name="import_warning">Le contenu de la sauvegarde sera AJOUTÉ aux données actuelles (aucune suppression).</string>
|
||||
<string name="import_warning">Le backup REMPLACERA toutes les données actuelles : les traitements, doses et analyses existants sont effacés d\'abord. Exporte une sauvegarde avant si tu veux les garder.</string>
|
||||
|
||||
<string name="about_title">À propos</string>
|
||||
<string name="models_credit">Profils PK issus des modèles d\'injection Estrannaise (EstraNase) et Transfem Science, extraits de Estrogen.ods (tables horaires D, k1–k3).</string>
|
||||
|
||||
@ -149,10 +149,10 @@
|
||||
<string name="export_ok">Backup exported.</string>
|
||||
<string name="export_fail">Export failed.</string>
|
||||
<string name="import_fail">Import failed: invalid file.</string>
|
||||
<string name="import_ok">Imported: %1$d treatments, %2$d doses, %3$d labs.</string>
|
||||
<string name="import_confirm">Restore</string>
|
||||
<string name="import_ok">Restored: %1$d treatments, %2$d doses, %3$d labs (current data was replaced, reminders rescheduled).</string>
|
||||
<string name="import_confirm">Erase & restore</string>
|
||||
<string name="import_title">Import backup</string>
|
||||
<string name="import_warning">The backup content will be ADDED to the current data (no deletion).</string>
|
||||
<string name="import_warning">The backup will REPLACE all current data: existing treatments, doses and labs are erased first. Export a backup now if you want to keep them.</string>
|
||||
|
||||
<string name="about_title">About</string>
|
||||
<string name="models_credit">PK profiles from Estrannaise (EstraNase) and Transfem Science injection models, extracted from Estrogen.ods (D, k1–k3 hourly tables).</string>
|
||||
|
||||
@ -506,7 +506,10 @@ gauche (pg/mL), T droite (ng/mL). Échelle « nice » (`niceCeil` : 1/2/2.5/5/10
|
||||
partagée entre toutes les séries. Grille 4 lignes ; labels Y gauche/droite ; X : pas
|
||||
6 h/24 h/5 j selon plage (`SimpleDateFormat` HH'h' / dd/MM). Labs : cercles (E2) et
|
||||
carrés (T) orange + valeur, **T convertie en ng/mL** (`convertTToNgMl`) au rendu.
|
||||
Ligne verticale « maintenant ».
|
||||
Ligne verticale « maintenant ». **Pics/creux** (v1.2.3, valeurs v1.2.6) :
|
||||
triangles ▲▼ via `PharmacokineticEngine.detectExtrema` **accompagnés de la
|
||||
valeur estimée** du pic (au-dessus) ou du creux (en dessous), dans la couleur
|
||||
de la série (`drawExtremum`).
|
||||
|
||||
Pièges :
|
||||
- `DrawScope` implémente `Density` → `X.dp.toPx()` direct ; ne PAS écrire de helper custom
|
||||
@ -751,6 +754,32 @@ JSON casse UNIQUEMENT en release. ⚠️ À chaque activation d'optimisation :
|
||||
tester sur téléphone l'export/import de backup + les graphiques (R8 ne se
|
||||
vérifie pas en tests JVM).
|
||||
|
||||
### Checklist de déploiement complète (de A à Z, pour une session sans contexte)
|
||||
|
||||
1. **Bumper la version** dans `app/build.gradle.kts` : `versionCode = N+1`,
|
||||
`versionName = "X.Y.Z+1"` (SemVer : fix = Z, feature = Y).
|
||||
2. **Tests verts obligatoires** : `./gradlew testDebugUnitTest` — 62 au total,
|
||||
44 si `local-test-data/` est absent (les régressions réelles sont skippées).
|
||||
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
|
||||
corrigé, + §2 (historique) si notable.
|
||||
4. **Commit** (message descriptif par couche) + **tag annoté** :
|
||||
`git tag -a vX.Y.Z -m "résumé"`.
|
||||
5. **Push** : `git push origin main --tags` + `git push farewell main --tags`
|
||||
(farewell : le repo doit exister sur l'instance ; push-to-create désactivé).
|
||||
6. **Construire les 2 APK au niveau du tag** :
|
||||
`git checkout vX.Y.Z` → `./gradlew assembleRelease assembleDebug` → copier
|
||||
`app-release.apk` → `/tmp/apks/HormoneTrack-vX.Y.Z-release.apk` et
|
||||
`app-debug.apk` → `HormoneTrack-vX.Y.Z-debug.apk` → `git checkout main`.
|
||||
7. **Publier les releases** (corps = section CHANGELOG + les 2 APK attachés) :
|
||||
`python3 scripts/gitea-release.py cloudyfy vX.Y.Z <release.apk>` puis
|
||||
idem avec `<debug.apk>` ; répéter avec l'instance `farewell` (token
|
||||
`gitea.farewell.dev` requis dans le trousseau, absent à ce jour).
|
||||
8. **Smoke-test R8 sur téléphone** (le release APK n'est pas vérifiable en
|
||||
tests JVM) : installation par-dessus l'existant, graphiques, export ET
|
||||
import d'un backup JSON, rappel. Le debug APK est le repli (même signature).
|
||||
9. **Mettre à jour §2/§8/§19/§21 si besoin** puis pusher la doc.
|
||||
|
||||
Notes :
|
||||
- L'APK est une **build debug** signée avec la clé debug locale — installable en
|
||||
sideload, mises à jour entre versions OK (même signature) ;
|
||||
@ -864,4 +893,4 @@ Sur le téléphone de test (à compléter par l'utilisatrice) :
|
||||
sur le graphique** et la calibration couvre toujours ses périodes
|
||||
|
||||
---
|
||||
*Doc mise à jour le 5 sept. 2026 (v1.2.5) — build OK, 62/62 tests verts (44 sans les données locales), dépôt Gitea privé + releases APK, aucune donnée de santé dans le dépôt ni l'historique.*
|
||||
*Doc mise à jour le 5 sept. 2026 (v1.2.6) — build OK, 62/62 tests verts (44 sans les données locales), dépôts Gitea (cloudyfy + farewell) avec releases APK, aucune donnée de santé dans le dépôt ni l'historique.*
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user