diff --git a/README.md b/README.md index ef4b455..d016085 100644 --- a/README.md +++ b/README.md @@ -17,12 +17,12 @@ > Fie-toi toujours à tes analyses de sang et aux consignes de ton > endocrinologue. -- **Statut** : web v1.9.6 — parité fonctionnelle avec l'Android v1.9.6 +- **Statut** : web v1.9.7 — parité fonctionnelle avec l'Android v1.9.7 (modèle Estrannaise analytique + nuage d'incertitude MCMC exclusif ESE ; presets des 6 esters injectables ESE ; l'auto-backup journalier Android reste structurellement non porté, cf §12 — l'export manuel couvre la donnée) · - **178 tests verts** · E2E navigateur ✅ · conteneur testé en + **180 tests verts** · E2E navigateur ✅ · conteneur testé en configuration durcie ✅ · lint/i18n ✅ - **Journal des versions web** : [docs/CHANGELOG.md](docs/CHANGELOG.md) - **Releases** : [gitea.cloudyfy.fr/Siphonight/HormoneTrack-web/releases](https://gitea.cloudyfy.fr/Siphonight/HormoneTrack-web/releases) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 2a0aeb9..8eec395 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -5,6 +5,20 @@ > section la plus récente EN PREMIER ; ce fichier est lu par le dialog > « Nouveautés » de l'app web (fetch `docs/CHANGELOG.md`). +## [1.9.7] — 2026-09-20 + +### Corrigé — la suggestion de prise de sang « change tout le temps » à chaque injection + +- Miroir de l'Android v1.9.7 (remontée + données réelles) : la règle de + stabilisation 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 recommandé fuyant). +- **Fix (tolérance)** : chaque écart doit rester dans **l'intervalle de + Posologie ± 24 h** (`GAP_TOLERANCE_MS`, fenêtre vs l'intervalle + THÉORIQUE). Un vrai changement de créneau (2 j au lieu de 7 j) reste + hors fenêtre. Tests : « interval change » ré-épinglé (+21 j) + 2 tests + de tolérance miroirs (12 verts lab-timing). + ## [1.9.6] — 2026-09-20 ### Modifié — la carte de l'accueil liste TOUTES les prochaines prises diff --git a/js/pk/lab-timing.js b/js/pk/lab-timing.js index c43661f..71107be 100644 --- a/js/pk/lab-timing.js +++ b/js/pk/lab-timing.js @@ -159,8 +159,16 @@ export function nextBloodDrawRecommendation(treatments, doseLogs, labs, nowMs = * modifiée/irrégulière) change le creux — conservateur assumé : une série * d'intervalles chaotiques maintient la carte « non stabilisée », ce qui est * pharmacocinétiquement vrai (le trough n'est comparable que sur un - * intervalle régulier). Comparaison EXACTE (au ms) : 7 j ± 1 h casse le - * régime, reflétant un vrai changement de créneau. + * intervalle régulier). + * + * v1.9.7 (miroir du Kotlin, remontée : « la suggestion change tout le temps + * à chaque injection si l'injection n'est pas faite pile à la même heure ») : + * la comparaison EXACTE cassait le régime pour un log à 12:30 au lieu de + * 12:00 — chaque écart doit rester dans l'INTERVALLE DE POSOLOGIE ± 24 h + * (« je m'injecte le même jour, à l'heure près »). La fenêtre se réfère à + * l'intervalle THÉORIQUE, pas à l'écart précédent : des logs à 6,8 j puis + * 7,2 j ne se déstabilisent plus en cascade. Un vrai changement (2 j au lieu + * de 7 j) reste hors fenêtre. * * @private * @param {object} carrier traitement porteur @@ -172,17 +180,22 @@ function computeRegimeStartMs(carrier, myDoses) { let idx = sorted.length - 1; let regimeStart = sorted[idx].timestamp; let current = sorted[idx]; - // Écart EXIGÉ pour remonter : celui des deux doses les plus récentes - let gapExpected = -1; + // Fenêtre d'écart = INTERVALLE DE POSOLOGIE ± 24 h (la Posologie est un + // prérequis de la carte : absente → on s'arrête à la dernière dose). + const intervalDays = carrier.forecastIntervalDays; + if (!intervalDays || intervalDays <= 0) return regimeStart; + const intervalMs = Math.round(intervalDays * 24 * HOUR_MS); + const gapMin = intervalMs - GAP_TOLERANCE_MS; + const gapMax = intervalMs + GAP_TOLERANCE_MS; const DOSE_EPS = 1e-6; while (idx > 0) { const prev = sorted[idx - 1]; const gap = current.timestamp - prev.timestamp; const sameMarker = String(doseEster(carrier, current)) === String(doseEster(carrier, prev)) && Math.abs(current.doseAmount - prev.doseAmount) < DOSE_EPS; - const sameGap = gapExpected < 0 || gap === gapExpected; + // gap > 0 : un doublon de log n'est jamais un régime + const sameGap = gap > 0 && gap >= gapMin && gap <= gapMax; if (!sameMarker || !sameGap) break; - gapExpected = gap; regimeStart = prev.timestamp; current = prev; idx--; @@ -190,6 +203,11 @@ function computeRegimeStartMs(carrier, myDoses) { return regimeStart; } +/** Tolérance de l'écart inter-doses (v1.9.7, miroir GAP_TOLERANCE_MS Kotlin) : + * ± 24 h autour de l'intervalle de Posologie — un log à 12:30 au lieu de + * 12:00 ne repousse plus la stabilisation de 5 × t½. */ +const GAP_TOLERANCE_MS = 24 * HOUR_MS; + /** * Invite d'AFFICHAGE (v1.8.0, demande explicite) : existe-t-il un traitement * E2 injectable ACTIF SANS Posologie ? Si oui, la page Analyses suggère de la diff --git a/js/ui/settings.js b/js/ui/settings.js index 22237a5..bb8c59b 100644 --- a/js/ui/settings.js +++ b/js/ui/settings.js @@ -31,7 +31,7 @@ import { el, showDialog, field, switchRow, filterChip, toast } from './component * (APK + web au MÊME numéro, tags appariés `vX.Y.Z` + `vX.Y.Z-web`). * v1.4.10 = portage initial de l'Android v1.4.10. */ -export const WEB_VERSION = '1.9.6'; +export const WEB_VERSION = '1.9.7'; /** Parse un champ numérique FR (virgule → point) ; null si vide/invalide. */ function parseNumOrNull(text) { diff --git a/tests/lab-timing.test.js b/tests/lab-timing.test.js index 1484950..31b9c0e 100644 --- a/tests/lab-timing.test.js +++ b/tests/lab-timing.test.js @@ -113,12 +113,35 @@ describe('LabTiming (miroir Android v1.8.0)', () => { }); test('changement d\'intervalle récent réinitialise la stabilisation', () => { - // 9 semaines à 7 j, puis une dose 2 j après la plus ancienne des deux - // dernières : l'écart terminal devient 2 j → régime fondé sur cette dose - // (celle à NOW−14 j avait un écart de 5 j ≠ 2 j → hors régime) + // 9 semaines à 7 j, puis une dose 2 j après la précédente : l'écart de + // 2 j (48 h) est HORS de la fenêtre Posologie ± 24 h (v1.9.7, [6 j ; + // 8 j] pour 7 j) → le régime s'arrête à la dose NOW−7 j (un vrai + // changement de créneau reste un changement — miroir Android) const doses = weeklyDoses(9).concat([makeDose(NOW - 9 * DAY_MS, 5.0)]); const rec = nextBloodDrawRecommendation([eenTfs()], doses, [], NOW); - assert.equal(rec.injectionMs, NOW + 14 * DAY_MS); + // régimeStart = NOW−7 j → stabilisation ≈ NOW+15,7 j → 1ᵉʳ créneau : +21 j + assert.equal(rec.injectionMs, NOW + 21 * DAY_MS); + assert.equal(rec.wasAlreadyStabilized, false); + }); + + test('le flou d\'heure de log ne réinitialise PAS la stabilisation (v1.9.7)', () => { + // REMONTÉE v1.9.7 (miroir Android) : « la suggestion change tout le + // temps à chaque injection si l'injection n'est pas faite pile à la + // même heure » — chaque écart reste dans Posologie ± 24 h → le régime + // remonte la séquence entière (63 j) → déjà stabilisée. + const jitterHours = [2, -3, 5, -1, 23, -23, 1, -0.5, 0.75]; + const doses = jitterHours.map((jh, i) => + makeDose(NOW - (63 - 7 * i) * DAY_MS + jh * HOUR_MS, 5.0)); + const rec = nextBloodDrawRecommendation([eenTfs()], doses, [], NOW); + assert.equal(rec.wasAlreadyStabilized, true); + }); + + test('un écart au-delà de 24 h de tolérance réinitialise (v1.9.7)', () => { + // 8 écarts réguliers, puis un retard de 25 h sur le 7 j : hors fenêtre + // → le régime ne remonte pas avant la dose en retard → non stabilisée + const doses = weeklyDoses(9).map((d, i) => + i === 7 ? makeDose(NOW - 14 * DAY_MS + 25 * HOUR_MS, 5.0) : d); + const rec = nextBloodDrawRecommendation([eenTfs()], doses, [], NOW); assert.equal(rec.wasAlreadyStabilized, false); });