v1.8.1 : fix stabilisation (changements récents vus) + encart Accueil (miroir Android)
- lab-timing.js : computeRegimeStartMs — le régime courant = séquence terminale de doses à (ester effectif, dose, écart inter-doses) constants (écart comparé EXACTEMENT) : tout changement récent de dose/ester/ posologie réinitialise la stabilisation. L'ancien proxy (1ʳᵉ dose du traitement) disait « stabilisée depuis février » malgré des changements récents (remontée v1.8.0). L'encart porte l'ESTER EFFECTIF de la dernière dose (override compris). - home.js : encart « Prochaine prise de sang (suggestion) » sur l'ACCUEIL (demande) — creux daté + créneau associé + mention de stabilisation seulement si le régime n'était pas déjà stable. - 3 nouveaux tests miroirs (155 verts) : dose/intervalle/ester → reset épinglé. WEB_VERSION 1.8.1. CHANGELOG [1.8.1], README.
This commit is contained in:
parent
f5951e1806
commit
61683ba5c4
10
README.md
10
README.md
@ -17,11 +17,11 @@
|
|||||||
> Fie-toi toujours à tes analyses de sang et aux consignes de ton
|
> Fie-toi toujours à tes analyses de sang et aux consignes de ton
|
||||||
> endocrinologue.
|
> endocrinologue.
|
||||||
|
|
||||||
- **Statut** : web v1.8.0 — parité fonctionnelle avec l'Android v1.8.0
|
- **Statut** : web v1.8.1 — parité fonctionnelle avec l'Android v1.8.1
|
||||||
(recommandation de prochaine prise de sang ; l'auto-backup journalier
|
(recommandation de prochaine prise de sang, stabilisation sensible aux
|
||||||
Android reste structurellement non porté, cf §12 — l'export manuel couvre
|
changements récents ; l'auto-backup journalier Android reste
|
||||||
la donnée) ·
|
structurellement non porté, cf §12 — l'export manuel couvre la donnée) ·
|
||||||
**152 tests verts** · E2E navigateur ✅ · conteneur testé en
|
**155 tests verts** · E2E navigateur ✅ · conteneur testé en
|
||||||
configuration durcie ✅ · lint/i18n ✅
|
configuration durcie ✅ · lint/i18n ✅
|
||||||
- **Journal des versions web** : [docs/CHANGELOG.md](docs/CHANGELOG.md)
|
- **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)
|
- **Releases** : [gitea.cloudyfy.fr/Siphonight/HormoneTrack-web/releases](https://gitea.cloudyfy.fr/Siphonight/HormoneTrack-web/releases)
|
||||||
|
|||||||
@ -5,6 +5,27 @@
|
|||||||
> 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.8.1] — 2026-09-17
|
||||||
|
|
||||||
|
### Corrigé — la stabilisation ignorait les changements récents (dose/ester/intervalle)
|
||||||
|
|
||||||
|
- **Bug remonté** : la recommandation disait « stabilisée depuis février »
|
||||||
|
alors que l'ester, le dosage ET la posologie venaient de changer —
|
||||||
|
l'ancien calcul partait de la **1ʳᵉ dose du traitement** (proxy aveugle
|
||||||
|
aux changements).
|
||||||
|
- **Fix** : le régime courant = la **séquence terminale de doses** à
|
||||||
|
(ester effectif, dose mg, **écart inter-doses**) constants — tout
|
||||||
|
changement récent réinitialise la stabilisation : la recommandation saute
|
||||||
|
au premier creux **après** la stabilisation du nouveau régime (règle des
|
||||||
|
~5 demi-vies conservée). L'encart porte l'ester **effectif** de la
|
||||||
|
dernière dose (override compris).
|
||||||
|
- **Nouveau : encart sur l'Accueil** (demande) — la suggestion de
|
||||||
|
prochaine prise de sang est désormais aussi visible directement sur la
|
||||||
|
page d'accueil (creux daté + créneau associé, mention de stabilisation
|
||||||
|
si le régime n'était pas déjà stable).
|
||||||
|
- 3 nouveaux tests miroirs (155 au total) : changement de dose / de
|
||||||
|
posologie (intervalle) / d'ester → réinitialisation épinglée.
|
||||||
|
|
||||||
## [1.8.0] — 2026-09-17
|
## [1.8.0] — 2026-09-17
|
||||||
|
|
||||||
### Prochaine prise de sang recommandée — portage de l'Android v1.8.0 (versions sync)
|
### Prochaine prise de sang recommandée — portage de l'Android v1.8.0 (versions sync)
|
||||||
|
|||||||
@ -34,7 +34,7 @@
|
|||||||
* ─────────────────────────────────────────────────────────────────────────────
|
* ─────────────────────────────────────────────────────────────────────────────
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { computeCurve, generateForecastDoses, TConfig, usesProfileModel } from './pk-engine.js';
|
import { computeCurve, generateForecastDoses, TConfig, usesProfileModel, doseEster } from './pk-engine.js';
|
||||||
import { model as tfsModel } from './transfem-science-models.js';
|
import { model as tfsModel } from './transfem-science-models.js';
|
||||||
import { model as whsahModel } from './whsah-models.js';
|
import { model as whsahModel } from './whsah-models.js';
|
||||||
import * as pkProfileStore from './pk-profile-store.js';
|
import * as pkProfileStore from './pk-profile-store.js';
|
||||||
@ -83,8 +83,15 @@ export function nextBloodDrawRecommendation(treatments, doseLogs, labs, nowMs =
|
|||||||
// ── 3) Début du régime courant + date de stabilisation ────────────────────
|
// ── 3) Début du régime courant + date de stabilisation ────────────────────
|
||||||
const myDoses = doseLogs.filter((d) => d.treatmentId === carrier.id);
|
const myDoses = doseLogs.filter((d) => d.treatmentId === carrier.id);
|
||||||
if (myDoses.length === 0) return null;
|
if (myDoses.length === 0) return null;
|
||||||
const regimeStartMs = Math.min(...myDoses.map((d) => d.timestamp));
|
const regimeStartMs = computeRegimeStartMs(carrier, myDoses);
|
||||||
const stabilizedAtMs = regimeStartMs + Math.round(STABILIZATION_HALF_LIVES * tHalfDays * 24 * HOUR_MS);
|
const stabilizedAtMs = regimeStartMs + Math.round(STABILIZATION_HALF_LIVES * tHalfDays * 24 * HOUR_MS);
|
||||||
|
// Ester EFFECTIF de la dernière dose (override compris) : c'est lui qui
|
||||||
|
// gouverne les creux futurs et le texte de la carte
|
||||||
|
let lastDose = myDoses[0];
|
||||||
|
for (const d of myDoses) {
|
||||||
|
if (d.timestamp > lastDose.timestamp) lastDose = d;
|
||||||
|
}
|
||||||
|
const activeEster = String(doseEster(carrier, lastDose)).toUpperCase();
|
||||||
|
|
||||||
// ── 4) Créneaux prévisionnels : assez pour couvrir la stabilisation ───────
|
// ── 4) Créneaux prévisionnels : assez pour couvrir la stabilisation ───────
|
||||||
const intervalDays = carrier.forecastIntervalDays;
|
const intervalDays = carrier.forecastIntervalDays;
|
||||||
@ -123,7 +130,7 @@ export function nextBloodDrawRecommendation(treatments, doseLogs, labs, nowMs =
|
|||||||
return {
|
return {
|
||||||
troughMs: trough.timestamp,
|
troughMs: trough.timestamp,
|
||||||
injectionMs: slot.timestamp,
|
injectionMs: slot.timestamp,
|
||||||
ester: carrier.esterType,
|
ester: activeEster,
|
||||||
terminalHalfLifeDays: tHalfDays,
|
terminalHalfLifeDays: tHalfDays,
|
||||||
regimeStartMs,
|
regimeStartMs,
|
||||||
stabilizedAtMs,
|
stabilizedAtMs,
|
||||||
@ -144,6 +151,53 @@ export function nextBloodDrawRecommendation(treatments, doseLogs, labs, nowMs =
|
|||||||
* @param {object[]} treatments
|
* @param {object[]} treatments
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* DÉBUT DU RÉGIME COURANT (v1.8.1 — correction du proxy v1.8.0, miroir du
|
||||||
|
* Kotlin). Critique remontée : « changé d'ester, de dosage ET de posologie,
|
||||||
|
* et l'app me disait stabilisée depuis février » — l'ancien proxy (1ʳᵉ dose
|
||||||
|
* du traitement) ne voyait aucun de ces changements.
|
||||||
|
*
|
||||||
|
* NOUVELLE RÈGLE : le régime courant = la séquence terminale de doses où
|
||||||
|
* (a) l'ESTER EFFECTIF et la DOSE (mg) sont identiques à la dose la plus
|
||||||
|
* récente, ET (b) l'ÉCART entre doses consécutives est constant (= l'écart
|
||||||
|
* des deux doses les plus récentes). On remonte depuis la dose la plus
|
||||||
|
* récente tant que ces conditions tiennent.
|
||||||
|
*
|
||||||
|
* Pourquoi l'écart fait partie du régime : 7 j → 2 j → 9 j (posologie
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {object} carrier traitement porteur
|
||||||
|
* @param {object[]} myDoses doses du porteur (ordre indifférent)
|
||||||
|
* @returns {number} timestamp de la 1ʳᵉ dose du régime courant
|
||||||
|
*/
|
||||||
|
function computeRegimeStartMs(carrier, myDoses) {
|
||||||
|
const sorted = myDoses.slice().sort((a, b) => a.timestamp - b.timestamp);
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
if (!sameMarker || !sameGap) break;
|
||||||
|
gapExpected = gap;
|
||||||
|
regimeStart = prev.timestamp;
|
||||||
|
current = prev;
|
||||||
|
idx--;
|
||||||
|
}
|
||||||
|
return regimeStart;
|
||||||
|
}
|
||||||
|
|
||||||
export function shouldSuggestPosology(treatments) {
|
export function shouldSuggestPosology(treatments) {
|
||||||
return treatments.some((t) =>
|
return treatments.some((t) =>
|
||||||
t.type === 'ESTRADIOL'
|
t.type === 'ESTRADIOL'
|
||||||
|
|||||||
@ -18,10 +18,10 @@
|
|||||||
* ─────────────────────────────────────────────────────────────────────────────
|
* ─────────────────────────────────────────────────────────────────────────────
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { computeCurve, autoCalibrated, nextReminderFireFor, Alerts, TConfig, pointHoursBefore } from '../pk/index.js';
|
import { computeCurve, autoCalibrated, nextReminderFireFor, Alerts, TConfig, pointHoursBefore, nextBloodDrawRecommendation } from '../pk/index.js';
|
||||||
import { store } from '../data/store.js';
|
import { store } from '../data/store.js';
|
||||||
import { t } from '../util/i18n.js';
|
import { t } from '../util/i18n.js';
|
||||||
import { formatDose, formatValue, formatTime, formatWeekdayTime, HrtDuration } from '../util/format.js';
|
import { formatDose, formatValue, formatTime, formatWeekdayTime, formatDateTime, HrtDuration } from '../util/format.js';
|
||||||
import { el } from './components.js';
|
import { el } from './components.js';
|
||||||
import { drawChart } from './chart-canvas.js';
|
import { drawChart } from './chart-canvas.js';
|
||||||
import { openDoseDialog } from './dialogs.js';
|
import { openDoseDialog } from './dialogs.js';
|
||||||
@ -138,6 +138,26 @@ export function renderHome(container, ctx) {
|
|||||||
col.appendChild(card);
|
col.appendChild(card);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Recommandation de prochaine prise de sang (v1.8.1) ───────────────────
|
||||||
|
// Même calcul que la page Analyses (pk/LabTiming.js) — encart COMPACT sur
|
||||||
|
// l'accueil (demande v1.8.1) : creux daté + créneau associé + mention de
|
||||||
|
// stabilisation SEULEMENT si le régime n'était pas déjà stable (sinon le
|
||||||
|
// texte est identique à chaque ouverture). Caché si non calculable.
|
||||||
|
const labRec = nextBloodDrawRecommendation(treatments, doseLogs, labResults, nowMs);
|
||||||
|
if (labRec) {
|
||||||
|
const recCard = el('div', { class: 'card variant' });
|
||||||
|
recCard.appendChild(el('div', { class: 'label' }, t('labrec_section')));
|
||||||
|
recCard.appendChild(el('div', { style: 'font-weight:600;' },
|
||||||
|
t('labrec_when', formatWeekdayTime(labRec.troughMs))));
|
||||||
|
recCard.appendChild(el('div', {},
|
||||||
|
t('labrec_before_injection', labRec.ester, formatWeekdayTime(labRec.injectionMs))));
|
||||||
|
if (!labRec.wasAlreadyStabilized) {
|
||||||
|
recCard.appendChild(el('div', { class: 'legend-muted' },
|
||||||
|
t('labrec_not_stabilized', labRec.ester, formatDateTime(new Date(labRec.stabilizedAtMs)))));
|
||||||
|
}
|
||||||
|
col.appendChild(recCard);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Log rapide : chips des traitements ACTIFS uniquement (saisie) ────────
|
// ── Log rapide : chips des traitements ACTIFS uniquement (saisie) ────────
|
||||||
const quickCard = el('div', { class: 'card variant' });
|
const quickCard = el('div', { class: 'card variant' });
|
||||||
quickCard.appendChild(el('h3', {}, t('logged_today')));
|
quickCard.appendChild(el('h3', {}, t('logged_today')));
|
||||||
|
|||||||
@ -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.8.0';
|
export const WEB_VERSION = '1.8.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) {
|
||||||
|
|||||||
@ -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.8.0'), 'dialog : section de la version courante');
|
ok(dlgText.includes('Version 1.8.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');
|
||||||
@ -298,7 +298,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.8.0 (web)'), 'Paramètres : version affichée');
|
ok(text.includes('Version 1.8.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') });
|
||||||
|
|
||||||
|
|||||||
@ -94,4 +94,43 @@ describe('LabTiming (miroir Android v1.8.0)', () => {
|
|||||||
assert.equal(shouldSuggestPosology([eenTfs(null)]), true);
|
assert.equal(shouldSuggestPosology([eenTfs(null)]), true);
|
||||||
assert.equal(shouldSuggestPosology([eenTfs(7.0)]), false);
|
assert.equal(shouldSuggestPosology([eenTfs(7.0)]), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── v1.8.1 : le régime courant voit les CHANGEMENTS RÉCENTS ──────────────
|
||||||
|
// Critique v1.8.0 : « changé d'ester, de dosage ET de posologie, et l'app
|
||||||
|
// me disait stabilisée depuis février » — l'ancien proxy (1ʳᵉ dose du
|
||||||
|
// traitement) ne détectait rien. NOUVELLE règle : le régime = séquence
|
||||||
|
// terminale à (ester, dose, écart inter-doses) constants ; tout changement
|
||||||
|
// récent réinitialise la stabilisation.
|
||||||
|
|
||||||
|
test('changement de dose récent réinitialise la stabilisation', () => {
|
||||||
|
// 9 semaines à 5 mg, puis 3 mg il y a 2 j : le régime (3 mg) n'a que
|
||||||
|
// 2 j → non stabilisé → créneau ≥ NOW−2 j + ~22,7 j ≈ NOW+26 j (le 1ᵉʳ
|
||||||
|
// créneau part de la DERNIÈRE dose réelle, pas de NOW)
|
||||||
|
const doses = weeklyDoses(9).concat([makeDose(NOW - 2 * DAY_MS, 3.0)]);
|
||||||
|
const rec = nextBloodDrawRecommendation([eenTfs()], doses, [], NOW);
|
||||||
|
assert.equal(rec.injectionMs, NOW + 26 * DAY_MS);
|
||||||
|
assert.equal(rec.wasAlreadyStabilized, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
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)
|
||||||
|
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);
|
||||||
|
assert.equal(rec.wasAlreadyStabilized, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('override d\'ester récent réinitialise la stabilisation (ester effectif)', () => {
|
||||||
|
// Même dose (5 mg), même écart (7 j), mais la dernière dose est un
|
||||||
|
// override EV : switch d'ester = nouveau régime + l'ester EFFECTIF
|
||||||
|
// (EV, pas EEN) alimente la carte
|
||||||
|
const doses = weeklyDoses(9).concat([
|
||||||
|
Object.assign(makeDose(NOW, 5.0), { esterType: 'EV' }),
|
||||||
|
]);
|
||||||
|
const rec = nextBloodDrawRecommendation([eenTfs()], doses, [], NOW);
|
||||||
|
assert.equal(rec.ester, 'EV');
|
||||||
|
assert.equal(rec.wasAlreadyStabilized, false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user