diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 22797a3..f5f8d4f 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -5,6 +5,19 @@ > 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.1] — 2026-09-19 + +### Corrigé — le nuage d'incertitude entoure maintenant la courbe Estrannaise calibrée + +- Miroir du fix Android v1.9.1 : la courbe ESE affichée est calibrée + (scalePerEster des labs) mais le nuage était tracé brut → échelles + différentes. Le nuage reçoit maintenant le MÊME scalePerEster que la + courbe ESE. +- Harmonisation de l'arrondi de la médiane de calibration (le web avait + la troncature historique) : les niveaux estimés web et Android + convergent. Le niveau correct = l'ARRONDI (plus juste que la + troncature, cf CHANGELOG Android 1.9.1). + ## [1.9.0] — 2026-09-18 ### Modèle Estrannaise ANALYTIQUE + nuage d'incertitude (portage Android v1.9.0, versions sync) diff --git a/js/pk/estrannaise-cloud.js b/js/pk/estrannaise-cloud.js index df00e03..d8a01de 100644 --- a/js/pk/estrannaise-cloud.js +++ b/js/pk/estrannaise-cloud.js @@ -21,7 +21,7 @@ */ import * as EstrannaiseModels from './estrannaise-models.js'; -import { usesProfileModel } from './pk-engine.js'; +import { usesProfileModel, doseEster } from './pk-engine.js'; /** Nombre de courbes du nuage (compromis étendue visuelle / coût ×32). */ @@ -45,7 +45,15 @@ export const MIN_HORIZON_HOURS = 6; * ESE couvert, aucune dose, ou MCMC non chargé. */ export function compute( - treatments, doseLogs, startMs, endMs, stepMs, nowMs, nbCurves = DEFAULT_NB_CURVES, + treatments, doseLogs, startMs, endMs, stepMs, nowMs, + nbCurves = DEFAULT_NB_CURVES, + /** + * Calibration à appliquer au nuage (v1.9.1 — fix « le nuage ne s'active + * que autour du tracé, pas autour du modèle Estrannaise ») : passer le + * MÊME scalePerEster que la courbe ESE pour que le nuage ENTOURE la + * courbe calibrée. + */ + scalePerEster = null, ) { if (startMs >= endMs || stepMs <= 0 || nbCurves < 2) return []; @@ -91,9 +99,11 @@ export function compute( if (dose.timestamp > t) continue; const dtH = (t - dose.timestamp) / 3600000; const tr = carriers.find((x) => x.id === dose.treatmentId); + // Même calibration que la courbe ESE (fix v1.9.1) + const scale = scalePerEster?.[doseEster(tr, dose)] ?? tr.scaleFactor; e2 += EstrannaiseModels.sampleWithParams( tr.esterType, dtH, paramsByEster.get(tr.esterType), - ) * dose.doseAmount; + ) * dose.doseAmount * scale; } curve.push({ timestamp: t, e2, t: 0.0 }); } diff --git a/js/ui/chart.js b/js/ui/chart.js index 6ce0275..77994ed 100644 --- a/js/ui/chart.js +++ b/js/ui/chart.js @@ -31,7 +31,7 @@ import { computeCurve, generateForecastDoses, autoCalibrated, TConfig, - computeLabAnchoredCurve, computeEstrannaiseCloud, + computeLabAnchoredCurve, computeEstrannaiseCloud, DEFAULT_NB_CURVES, } from '../pk/index.js'; import { stepForRange, defaultModelToggles, panDeltaHours, clampPanHours, @@ -314,6 +314,7 @@ export function renderChart(container, ctx, { preserveState = false } = {}) { if (state.showCloud) { const cloud = computeEstrannaiseCloud( treatments, doseLogs, startMs, endMs, step, nowMs, + DEFAULT_NB_CURVES, autoByModel.get('ESE')?.esterScales ?? null, ); if (cloud.length > 0) curves.push(['CLOUD', cloud.flat()]); } diff --git a/js/ui/settings.js b/js/ui/settings.js index a6fc2db..7c7d30d 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.0'; +export const WEB_VERSION = '1.9.1'; /** Parse un champ numérique FR (virgule → point) ; null si vide/invalide. */ function parseNumOrNull(text) { diff --git a/scripts/e2e.mjs b/scripts/e2e.mjs index 88cac77..6f89581 100644 --- a/scripts/e2e.mjs +++ b/scripts/e2e.mjs @@ -131,7 +131,7 @@ async function main() { ok(dialog !== null, 'dialog « Nouveautés » au premier lancement (markdown rendu)'); if (dialog) { const dlgText = await dialog.textContent(); - ok(dlgText.includes('Version 1.9.0'), 'dialog : section de la version courante'); + ok(dlgText.includes('Version 1.9.1'), 'dialog : section de la version courante'); const hasRenderedMd = await dialog.$('.md-body h3, .md-body strong'); ok(hasRenderedMd !== null, 'dialog : markdown RENDU (titres/gras), pas de brut'); await page.click('.dialog .actions button'); @@ -334,7 +334,7 @@ async function main() { await page.waitForTimeout(300); text = await page.textContent('main.content'); ok(text.includes('Langue'), 'Paramètres : carte langue'); - ok(text.includes('Version 1.9.0 (web)'), 'Paramètres : version affichée'); + ok(text.includes('Version 1.9.1 (web)'), 'Paramètres : version affichée'); ok(text.includes('100 % local'), 'Paramètres : note vie privée'); await page.screenshot({ path: join(SHOTS, 'e2e-settings.png') }); diff --git a/tests/estrannaise-cloud.test.js b/tests/estrannaise-cloud.test.js index 773bb7d..1d59bbe 100644 --- a/tests/estrannaise-cloud.test.js +++ b/tests/estrannaise-cloud.test.js @@ -66,4 +66,20 @@ describe('EstrannaiseCloud web (miroir Android v1.9.0)', () => { test('nbCurves < 2 → nuage vide', () => { assert.equal(compute([ese()], doses(), NOW - 30 * DAY_MS, NOW, 3_600_000, NOW, 1).length, 0); }); + + test('le nuage suit la calibration (fix v1.9.1, miroir Android)', () => { + // scalePerEster ×2 → nuage calibré = nuage brut ×2 (le nuage ENTOURE + // la courbe ESE calibrée au lieu de flotter à une autre échelle) + const scaled = { EEN: 2.0 }; + const calibrated = compute([ese()], doses(), NOW - 30 * DAY_MS, NOW, 3_600_000, NOW, 32, scaled); + const raw = compute([ese()], doses(), NOW - 30 * DAY_MS, NOW, 3_600_000, NOW); + assert.equal(calibrated.length, raw.length); + assert.ok(raw[raw.length - 1].length >= 2); + const rawLast = raw[raw.length - 1][raw[raw.length - 1].length - 1].e2; + const calLast = calibrated[calibrated.length - 1][calibrated[calibrated.length - 1].length - 1].e2; + assert.ok( + Math.abs(calLast - rawLast * 2) <= rawLast * 0.01, + `calibré (${calLast}) ≈ 2× brut (${rawLast})`, + ); + }); });