From f79a180e36c4b12eb73c45a20b0228ff5a2a0b28 Mon Sep 17 00:00:00 2001 From: Siphonight Date: Sat, 19 Sep 2026 23:29:40 +0200 Subject: [PATCH] =?UTF-8?q?v1.9.4=20(web)=20:=20presets=20ESE=20=C3=A9tend?= =?UTF-8?q?us=20=C3=A0=206=20esters=20+=20=C3=A9diteur=20dynamique=20+=20p?= =?UTF-8?q?ortage=20v1.9.0=20compl=C3=A9t=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - presets.js : EC huile (5 mg IM), EB (1 mg IM), EUCS suspension cristalline (10 mg SC) — 25 entrées, champs Bateman informatifs - pk-engine.js : Esters.EUCS manquant (portage v1.9.0 incomplet — le preset référençait undefined) - models.js : choicesForModel ESE = 6 esters (parité Android v1.9.0 ; était resté sur la couverture des tables ODS à 3) + KDoc à jour - treatment-editor.js : liste des esters = choicesForModel(model) — rebascule sur EV si l'ester sort de la couverture au changement de modèle (garde anti 0 pg/mL) + re-render du formulaire - tests/presets.test.js (nouveau, 6) : couverture 6 esters ESE, unicité, i18n EN/FR, TFS 7 / WHSAH 6 inchangés, choicesForModel aligné dispatchs - versions 1.9.4 (settings + e2e) + CHANGELOG/§4/§8/README --- README.md | 9 ++--- docs/CHANGELOG.md | 24 ++++++++++++++ docs/DEVELOPPEMENT.md | 12 ++++++- js/data/models.js | 7 ++-- js/pk/pk-engine.js | 1 + js/ui/settings.js | 2 +- js/ui/treatment-editor.js | 32 +++++++++++++----- scripts/e2e.mjs | 4 +-- tests/presets.test.js | 70 +++++++++++++++++++++++++++++++++++++++ 9 files changed, 142 insertions(+), 19 deletions(-) create mode 100644 tests/presets.test.js diff --git a/README.md b/README.md index bc840d1..0878614 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,12 @@ > Fie-toi toujours à tes analyses de sang et aux consignes de ton > endocrinologue. -- **Statut** : web v1.9.2 — parité fonctionnelle avec l'Android v1.9.2 +- **Statut** : web v1.9.4 — parité fonctionnelle avec l'Android v1.9.4 (modèle Estrannaise analytique + nuage d'incertitude MCMC exclusif ESE ; - l'auto-backup journalier Android reste structurellement non porté, cf - §12 — l'export manuel couvre la donnée) · - **169 tests verts** · E2E navigateur ✅ · conteneur testé en + presets des 6 esters injectables ESE ; l'auto-backup journalier Android + reste structurellement non porté, cf §12 — l'export manuel couvre la + donnée) · + **175 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 594f846..2bf3fd3 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -5,6 +5,30 @@ > 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.4] — 2026-09-19 + +### Ajouté — les presets du modèle ESE couvrent ses 6 esters injectables + +- **Remontée** : depuis l'ajout du modèle source Estrannaise (v1.9.0), les + presets de création n'offraient toujours que EV/EU/EEn — « les presets + n'ont pas été mis à jour lors de l'ajout du modèle source pour ESE ». +- **Presets** : 3 presets ESE ajoutés (25 entrées, miroir de + `PKPresets.kt`) — EC huile (5 mg IM), EB (1 mg IM), EUCS undécylate + suspension cristalline (10 mg SC, ester exclusif à ce fit). Champs + Bateman informatifs = t½ terminales des fits (pas utilisés au calcul). +- **Éditeur** : la liste des esters suit la couverture RÉELLE du modèle + choisi (`choicesForModel`, parité Android v1.9.0) — ESE liste ses 6 + esters injectables ; au changement de modèle, si l'ester courant sort de + la couverture (ex. EUCS + TFS), il rebascule sur EV (garde anti + « 0 pg/mL ») et le formulaire se re-rend. +- **Corrigé (portage v1.9.0 incomplet)** : `Esters.EUCS` manquait dans + `pk-engine.js` (le preset référençait `undefined`) et `choicesForModel` + (`js/data/models.js`) était resté sur la couverture des tables ODS + (3 esters) avec sa KDoc obsolète « produirait 0 pg/mL ». +- **Tests** : `tests/presets.test.js` (nouveau, 6 tests) — couverture des + 6 esters ESE, unicité, clés i18n EN/FR, TFS (7) / WHSAH (6) inchangés, + `choicesForModel` aligné sur les dispatchs. Suite web : 175 tests. + ## [1.9.3] — 2026-09-19 ### Corrigé — le numéro de version annoncé diff --git a/docs/DEVELOPPEMENT.md b/docs/DEVELOPPEMENT.md index 9ed3840..26a2d26 100644 --- a/docs/DEVELOPPEMENT.md +++ b/docs/DEVELOPPEMENT.md @@ -170,7 +170,8 @@ HormoneTrack-web/ (dépôt séparé) │ │ ρ log-linéaire, fenêtre labs) │ ├── alerts.js Alerts.kt : seuils opt-in, codec d'état, │ │ │ shouldNotify (anti-spam) -│ │ ├── presets.js PKPresets.kt (22 presets, clés i18n) +│ │ ├── presets.js PKPresets.kt (25 presets — v1.9.4 : +│ │ │ les 6 esters injectables ESE) │ │ ├── chart-helpers.kt fonctions pures de CurveChart.kt : │ │ │ panDeltaHours (#62), xLabelTicks (#55), │ │ │ pointHoursBefore (#53), clampPanHours (#54), @@ -251,6 +252,9 @@ HormoneTrack-web/ (dépôt séparé) │ ├── rounding.test.js ← round2/round3 arrondis (v1.9.2 : │ │ harmonisation Android, fin de la │ │ troncature historique) +│ ├── presets.test.js ← PKPresetsTest.kt (v1.9.4 : presets des +│ │ 6 esters ESE, unicité, i18n EN/FR, +│ │ choicesForModel aligné dispatchs) │ ├── lab-timing.test.js ← LabTimingTest.kt (v1.8.0 : prochaine │ │ prise de sang recommandée — creux avant │ │ créneau, saut de stabilisation, gardes) @@ -456,6 +460,12 @@ launch) — d'où le build dédié dans le cache Playwright, hors dépôt. exclusivité ESE — TFS/oral hors nuage, gardes vides) + **round2/round3 ARRONDIS** (v1.9.2 : fin de la troncature historique — harmonisation des niveaux web/Android, cf CHANGELOG Android 1.8.2) ; +- **presets** : v1.9.4 (miroir `PKPresetsTest.kt`) — les presets ESE + couvrent les 6 esters du fit analytique (unicité, clés i18n EN/FR), + TFS (7) / WHSAH (6) inchangés, et **`choicesForModel` aligné sur les + dispatchs** (ESE 6 / TFS 7 explicite sans EUCS / WHS 6) — l'éditeur et + le dialog de dose ne peuvent proposer un ester que le moteur sait + tracer ; - **backup** : round-trip v2, rétrocompat v1, écrasement + IDs conservés, **imports des exports Android réels** (data-driven, hors dépôt). - **chart-helpers** : cumul fractionnaire du pan (#62), minuit local (#55), diff --git a/js/data/models.js b/js/data/models.js index 62dfe03..c1775cf 100644 --- a/js/data/models.js +++ b/js/data/models.js @@ -40,10 +40,10 @@ export const AdministrationRoute = { /** * Esters proposables dans l'override par dose pour un MODÈLE PK donné - * (miroir de `Esters.choicesForModel`, v1.4.6 — la liste suit la couverture + * (miroir de `Esters.choicesForModel` — la liste suit la couverture RÉELLE * du modèle : TFS = 7 esters ; WHSAH = 6 (PEP non couvert) ; Estrannaise - * (tables ODS) = EV/EU/EEn seulement — un traitement ESE + EB produirait - * 0 pg/mL). + * ANALYTIQUE (v1.9.0) = 6 esters injectables, dont EUCS exclusif à ce fit — + * un ester hors liste produirait 0 pg/mL : aucun fallback silencieux). * * @param {string} pkModel * @returns {string[]} @@ -51,6 +51,7 @@ export const AdministrationRoute = { export function choicesForModel(pkModel) { if (pkModel === 'TFS') return ['EV', 'EU', 'EEN', 'EB', 'EC', 'ECS', 'PEP']; if (pkModel === 'WHS') return ['EV', 'EU', 'EEN', 'EB', 'EC', 'ECS']; + if (pkModel === 'ESE') return ['EV', 'EU', 'EEN', 'EC', 'EB', 'EUCS']; // v1.9.4 : analytique (parité Android v1.9.0) return ['EV', 'EU', 'EEN']; } diff --git a/js/pk/pk-engine.js b/js/pk/pk-engine.js index a7015cb..843603d 100644 --- a/js/pk/pk-engine.js +++ b/js/pk/pk-engine.js @@ -55,6 +55,7 @@ export class TConfig { export const Esters = { NONE: 'NONE', EV: 'EV', EU: 'EU', EEN: 'EEN', EB: 'EB', EC: 'EC', ECS: 'ECS', PEP: 'PEP', + EUCS: 'EUCS', // v1.9.4 : undécylate suspension cristalline (fit Estrannaise seulement) }; /** Constantes de modèles PK (miroir de `PKModels.kt`). */ diff --git a/js/ui/settings.js b/js/ui/settings.js index b788339..187a6e3 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.3'; +export const WEB_VERSION = '1.9.4'; /** Parse un champ numérique FR (virgule → point) ; null si vide/invalide. */ function parseNumOrNull(text) { diff --git a/js/ui/treatment-editor.js b/js/ui/treatment-editor.js index 5395f4f..e422478 100644 --- a/js/ui/treatment-editor.js +++ b/js/ui/treatment-editor.js @@ -2,8 +2,13 @@ * ───────────────────────────────────────────────────────────────────────────── * Éditeur de traitement (portage de `ui/screens/TreatmentEditorScreen.kt`). * - * - presets (PKPresets, 22 entrées) pré-remplissent tout (création) ; - * - champs conditionnels : ester + modèle PK si injection, Bateman sinon ; + * - presets (PKPresets, 25 entrées) pré-remplissent tout (création) — + * v1.9.4 : le modèle ESE offre ses 6 esters injectables (EV/EU/EEn/EC/ + * EB/EUCS), au même niveau que les presets TFS (7) et WHSAH (6) ; + * - champs conditionnels : ester + modèle PK si injection, Bateman sinon — + * v1.9.4 : la liste des esters suit la couverture RÉELLE du modèle + * choisi (choicesForModel) et rebascule sur EV si l'ester courant + * n'y est plus (garde anti « 0 pg/mL ») ; * - carte Calibration : facteur manuel + bouton « Calibrer avec les * analyses » (computeScaleFactor — médiane lab ÷ prédiction, garde #61) ; * hint quand l'auto-calibration écrase le facteur à l'affichage (v1.4.9) ; @@ -22,7 +27,7 @@ */ import { store } from '../data/store.js'; -import { newTreatment, isInjectionRoute, typeLabelKey, routeLabelKey, modelLabelKey } from '../data/models.js'; +import { newTreatment, isInjectionRoute, typeLabelKey, routeLabelKey, modelLabelKey, choicesForModel } from '../data/models.js'; import { all as allPresets } from '../pk/presets.js'; import { computeScaleFactor } from '../pk/pk-calibration.js'; import { t } from '../util/i18n.js'; @@ -124,14 +129,25 @@ export function renderTreatmentEditor(container, ctx) { })); if (isInjectionRoute(st.route)) { - // Ester : liste simple (miroir Android — EV/EU/EEn dans l'éditeur ; - // les 7 esters sont proposables dans l'OVERRIDE par dose du DoseDialog) - const esterOptions = ['EV', 'EU', 'EEN'].map((x) => ({ value: x, label: x })); + // Ester : liste = couverture RÉELLE du modèle choisi (v1.9.4 — + // miroir Android Esters.choicesForModel) : ESE offre ses 6 esters + // injectables depuis l'éditeur, au lieu des seuls EV/EU/EEn de + // l'époque des tables ODS. L'override par dose du dialog garde le + // même helper (pas de divergence des deux listes). + const esterOptions = choicesForModel(st.model).map((x) => ({ value: x, label: x })); form.appendChild(dropdown(t('ester'), st.ester, esterOptions, (v) => { st.ester = v; })); - // Modèle PK : 3 choix (v1.4.6 — WHSAH ajouté) + // Modèle PK : 3 choix (v1.4.6 — WHSAH ajouté). Au changement de + // modèle, l'ester courant peut sortir de la couverture (ex. EUCS + + // TFS : le V3C n'a pas de paramètres EUCS → 0 pg/mL) : on rebascule + // sur le premier ester couvert (EV partout) puis on re-rend le + // formulaire pour reconstruire la liste des esters (v1.9.4). const modelOptions = ['ESE', 'TFS', 'WHS'].map((x) => ({ value: x, label: t(modelLabelKey(x)) })); - form.appendChild(dropdown(t('pk_model'), st.model, modelOptions, (v) => { st.model = v; })); + form.appendChild(dropdown(t('pk_model'), st.model, modelOptions, (v) => { + st.model = v; + if (!choicesForModel(v).includes(st.ester)) st.ester = choicesForModel(v)[0]; + rerenderForm(); + })); } else { const row1 = el('div', { class: 'row' }); row1.appendChild(field({ label: t('pk_absorption'), type: 'number', value: st.tmaxText, onInput: (v) => { st.tmaxText = v; }, attrs: { step: 'any' } })); diff --git a/scripts/e2e.mjs b/scripts/e2e.mjs index bc39d91..1414540 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.3'), 'dialog : section de la version courante'); + ok(dlgText.includes('Version 1.9.4'), '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.3 (web)'), 'Paramètres : version affichée'); + ok(text.includes('Version 1.9.4 (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/presets.test.js b/tests/presets.test.js new file mode 100644 index 0000000..775b522 --- /dev/null +++ b/tests/presets.test.js @@ -0,0 +1,70 @@ +/** + * Tests des PRESETS de traitements (v1.9.4 — miroir de `PKPresets.kt`) : + * le modèle ESE ANALYTIQUE couvre 6 esters injectables — les presets de + * création doivent offrir les 6 (comme TFS offre les 7 du V3C et WHSAH + * les 6 du fit Mona). Avant v1.9.4, les presets ESE n'offraient que + * EV/EU/EEn (l'héritage des tables ODS) — remontée : « les presets n'ont + * pas été mis à jour lors de l'ajout du modèle source ESE ». + */ + +import { test, describe } from 'node:test'; +import assert from 'node:assert/strict'; +import { all } from '../js/pk/presets.js'; + +describe('PKPresets web (presets ESE étendus — v1.9.4)', () => { + test('les presets ESE couvrent les 6 esters du fit analytique', () => { + const eseInjectable = all.filter((p) => p.pkModel === 'ESE' && p.esterType !== 'NONE'); + const esters = eseInjectable.map((p) => p.esterType).sort(); + assert.deepEqual(esters, ['EB', 'EC', 'EEN', 'EU', 'EUCS', 'EV']); + }); + + test('chaque ester couvert a EXACTEMENT un preset ESE (pas de doublon)', () => { + const eseInjectable = all.filter((p) => p.pkModel === 'ESE' && p.esterType !== 'NONE'); + const names = eseInjectable.map((p) => p.nameKey); + assert.equal(new Set(names).size, names.length); + }); + + test('chaque preset a une clé i18n présente dans EN et FR', async () => { + const { store } = await import('../js/data/store.js'); + store.setBackend(new Map()); // Node : pas de localStorage + const i18n = await import('../js/util/i18n.js'); + // t() lit la langue du store (pas de setLanguage exporté) : on force + // successivement 'en' puis 'fr' via les settings (miroir de l'app). + for (const lang of ['en', 'fr']) { + store.setSettings({ language: lang }); + for (const p of all) { + // t() retombe sur la clé brute si absente : un libellé égal à la + // clé = clé manquante (leçon §12 web : une string manquante + // s'affiche brute dans l'UI) + assert.notEqual(i18n.t(p.nameKey), p.nameKey, `clé i18n manquante (${lang}) : ${p.nameKey}`); + } + } + }); + + test('les presets TFS (7) et WHSAH (6) restent inchangés', () => { + const injectables = (model) => all.filter((p) => p.pkModel === model && p.esterType !== 'NONE'); + assert.equal(injectables('TFS').length, 7); + assert.equal(injectables('WHS').length, 6); + }); + + test('choicesForModel (éditeur + dialog de dose) suit la couverture du fit ESE', async () => { + // v1.9.4 : l'éditeur liste choicesForModel(model) — si ce helper restait + // sur la couverture des tables ODS (3 esters), l'éditeur n'offrirait pas + // EC/EB/EUCS même avec les presets à jour (parité Android v1.9.0). + const { choicesForModel } = await import('../js/data/models.js'); + assert.deepEqual(choicesForModel('ESE').sort(), ['EB', 'EC', 'EEN', 'EU', 'EUCS', 'EV']); + // TFS reste EXPLICITE à 7 (pas d'EUCS dans le V3C — sinon 0 pg/mL) et + // WHSAH à 6 (PEP non couvert). + assert.deepEqual(choicesForModel('TFS').sort(), ['EB', 'EC', 'ECS', 'EEN', 'EU', 'EV', 'PEP']); + assert.deepEqual(choicesForModel('WHS').sort(), ['EB', 'EC', 'ECS', 'EEN', 'EU', 'EV']); + assert.equal(choicesForModel('TFS').includes('EUCS'), false); + }); + + test('les presets ESE non-injectables (gel/patch/oral) restent en Bateman', () => { + const eseNonInjectable = all.filter( + (p) => p.pkModel === 'ESE' && (p.esterType === 'NONE' || p.esterType == null), + ); + assert.ok(eseNonInjectable.length >= 3, 'gel/patch/oral toujours présents'); + assert.ok(eseNonInjectable.every((p) => p.esterType === 'NONE')); + }); +});