v1.9.1 : fix nuage calibré (il entoure la courbe ESE calibrée) + harmonisation arrondi (miroir Android)

- estrannaise-cloud.js : paramètre scalePerEster — le nuage reçoit la
  MÊME calibration que la courbe ESE (autoByModel ESE). Avant : courbe ESE
  calibrée mais nuage BRUT → échelles différentes (remontée v1.9.1 :
  « le nuage ne s'active que autour du tracé, pas autour du modèle
  Estrannaise »).
- chart.js : autoByModel.get('ESE')?.esterScales passé au nuage ;
  DEFAULT_NB_CURVES importé.
- +1 test : nuage calibré ×2 = nuage brut ×2 (167 verts au total).
- CHANGELOG [1.9.1] : fix nuage calibré + HARMONISATION de l'arrondi de
  la médiane de calibration (le web avait la troncature historique — les
  niveaux web/android convergent ; le niveau correct = l'ARRONDI).
- WEB_VERSION 1.9.1.
This commit is contained in:
Siphonight 2026-09-19 21:14:27 +02:00
parent 174928bb6b
commit fc1b6e1700
6 changed files with 47 additions and 7 deletions

View File

@ -5,6 +5,19 @@
> section la plus récente EN PREMIER ; ce fichier est lu par le dialog > section la plus récente EN PREMIER ; ce fichier est lu par le dialog
> « Nouveautés » de l'app web (fetch `docs/CHANGELOG.md`). > « 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 ## [1.9.0] — 2026-09-18
### Modèle Estrannaise ANALYTIQUE + nuage d'incertitude (portage Android v1.9.0, versions sync) ### Modèle Estrannaise ANALYTIQUE + nuage d'incertitude (portage Android v1.9.0, versions sync)

View File

@ -21,7 +21,7 @@
*/ */
import * as EstrannaiseModels from './estrannaise-models.js'; 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). */ /** 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é. * ESE couvert, aucune dose, ou MCMC non chargé.
*/ */
export function compute( 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 []; if (startMs >= endMs || stepMs <= 0 || nbCurves < 2) return [];
@ -91,9 +99,11 @@ export function compute(
if (dose.timestamp > t) continue; if (dose.timestamp > t) continue;
const dtH = (t - dose.timestamp) / 3600000; const dtH = (t - dose.timestamp) / 3600000;
const tr = carriers.find((x) => x.id === dose.treatmentId); 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( e2 += EstrannaiseModels.sampleWithParams(
tr.esterType, dtH, paramsByEster.get(tr.esterType), tr.esterType, dtH, paramsByEster.get(tr.esterType),
) * dose.doseAmount; ) * dose.doseAmount * scale;
} }
curve.push({ timestamp: t, e2, t: 0.0 }); curve.push({ timestamp: t, e2, t: 0.0 });
} }

View File

@ -31,7 +31,7 @@
import { import {
computeCurve, generateForecastDoses, autoCalibrated, TConfig, computeCurve, generateForecastDoses, autoCalibrated, TConfig,
computeLabAnchoredCurve, computeEstrannaiseCloud, computeLabAnchoredCurve, computeEstrannaiseCloud, DEFAULT_NB_CURVES,
} from '../pk/index.js'; } from '../pk/index.js';
import { import {
stepForRange, defaultModelToggles, panDeltaHours, clampPanHours, stepForRange, defaultModelToggles, panDeltaHours, clampPanHours,
@ -314,6 +314,7 @@ export function renderChart(container, ctx, { preserveState = false } = {}) {
if (state.showCloud) { if (state.showCloud) {
const cloud = computeEstrannaiseCloud( const cloud = computeEstrannaiseCloud(
treatments, doseLogs, startMs, endMs, step, nowMs, treatments, doseLogs, startMs, endMs, step, nowMs,
DEFAULT_NB_CURVES, autoByModel.get('ESE')?.esterScales ?? null,
); );
if (cloud.length > 0) curves.push(['CLOUD', cloud.flat()]); if (cloud.length > 0) curves.push(['CLOUD', cloud.flat()]);
} }

View File

@ -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`). * (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. * 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. */ /** Parse un champ numérique FR (virgule → point) ; null si vide/invalide. */
function parseNumOrNull(text) { function parseNumOrNull(text) {

View File

@ -131,7 +131,7 @@ async function main() {
ok(dialog !== null, 'dialog « Nouveautés » au premier lancement (markdown rendu)'); ok(dialog !== null, 'dialog « Nouveautés » au premier lancement (markdown rendu)');
if (dialog) { if (dialog) {
const dlgText = await dialog.textContent(); 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'); const hasRenderedMd = await dialog.$('.md-body h3, .md-body strong');
ok(hasRenderedMd !== null, 'dialog : markdown RENDU (titres/gras), pas de brut'); ok(hasRenderedMd !== null, 'dialog : markdown RENDU (titres/gras), pas de brut');
await page.click('.dialog .actions button'); await page.click('.dialog .actions button');
@ -334,7 +334,7 @@ async function main() {
await page.waitForTimeout(300); await page.waitForTimeout(300);
text = await page.textContent('main.content'); text = await page.textContent('main.content');
ok(text.includes('Langue'), 'Paramètres : carte langue'); 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'); ok(text.includes('100 % local'), 'Paramètres : note vie privée');
await page.screenshot({ path: join(SHOTS, 'e2e-settings.png') }); await page.screenshot({ path: join(SHOTS, 'e2e-settings.png') });

View File

@ -66,4 +66,20 @@ describe('EstrannaiseCloud web (miroir Android v1.9.0)', () => {
test('nbCurves < 2 → nuage vide', () => { test('nbCurves < 2 → nuage vide', () => {
assert.equal(compute([ese()], doses(), NOW - 30 * DAY_MS, NOW, 3_600_000, NOW, 1).length, 0); 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})`,
);
});
}); });