From 551d8fa29eeec20927d4e16062a616e2b53d3d87 Mon Sep 17 00:00:00 2001 From: Siphonight Date: Sat, 12 Sep 2026 23:51:20 +0200 Subject: [PATCH] v1.6.0 (web, UI + E2E) : chip Prolonger, split LAB/LABX, avertissement, WEB_VERSION 1.6.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - chart.js : chip « Prolonger » (off par défaut, désactivé tant que Tracé labs est off), split série "LAB" (ancrée) / "LABX" (prolongée, rose atténué rgba(194,24,91,0.55)), légendes dédiées (clés LAB ET LABX skippées de la boucle — miroir leçon #63), AVERTISSEMENT « simple simulation, sans garantie de correspondre au réel, labs potentiellement erronés » affiché sous la légende tant que la prolongation est visible (condition factorisée : jamais de courbe prolongée sans avertissement). - i18n : show_lab_track_extend, legend_lab_track_extend, lab_track_extend_warning (FR/EN synchronisées). - E2E : scénario v1.6.0 (chip désactivé → activation → légende + avertissement → disparition à la désactivation) ; assertions version 1.6.0. --- js/ui/chart.js | 61 +++++++++++++++++++++++++++++++++++++++++++---- js/ui/settings.js | 2 +- js/util/i18n.js | 12 ++++++++++ scripts/e2e.mjs | 55 ++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 122 insertions(+), 8 deletions(-) diff --git a/js/ui/chart.js b/js/ui/chart.js index 6e3dc59..f9ac70b 100644 --- a/js/ui/chart.js +++ b/js/ui/chart.js @@ -76,6 +76,13 @@ export function renderChart(container, ctx) { showForecast: false, showExtrema: false, showLabTrack: false, // v1.5.0 — courbe ancrée sur les labs, OFF par défaut (choix v1.5.0) + // v1.6.0 : PROLONGATION du tracé labs au-delà du dernier lab (ρ du + // dernier lab constant × modèle, bornée par l'extinction du modèle — + // cf LabTrajectoryModel.extensionHorizonEndMs). OFF par défaut : la + // partie prolongée est une ESTIMATION non ancrée, elle doit être + // demandée explicitement ; le chip n'est actif que si Lab track est ON + // (miroir ChartScreen.kt). + showLabTrackExtend: false, panResidualHours: 0.0, // résiduel fractionnaire du pan (fix #62) togglesInitialized: false, }; @@ -208,18 +215,39 @@ export function renderChart(container, ctx) { modelRow.appendChild(filterChip(t('model_whs'), state.showWhs, () => { state.showWhs = !state.showWhs; redraw(); })); // v1.5.0 : courbe hybride ancrée sur les labs (off par défaut) modelRow.appendChild(filterChip(t('show_lab_track'), state.showLabTrack, () => { state.showLabTrack = !state.showLabTrack; redraw(); })); + // v1.6.0 : prolongation du tracé labs au-delà du dernier lab (off par + // défaut, SANS EFFET — donc désactivé — tant que Lab track est off : + // la prolongation n'existe que comme extension de CETTE courbe) + modelRow.appendChild(filterChip(t('show_lab_track_extend'), state.showLabTrackExtend, () => { state.showLabTrackExtend = !state.showLabTrackExtend; redraw(); }, !state.showLabTrack)); // ── Courbes par modèle sélectionné ───────────────────────────────────── const curves = []; // ── v1.5.0 : courbe « Tracé labs » ─────────────────────────────────────── // Indépendante des toggles modèles (modèle STOCKÉ — sémantique Home) et // de la calibration (elle EST sa propre calibration continue — épinglé). + // + // ── v1.6.0 : split ancré / prolongé ────────────────────────────────────── + // Avec « Prolonger », la série est SPLITTÉE au dernier lab significatif + // (lastAnchorMs) : "LAB" = partie ancrée (passe par les labs), "LABX" = + // partie prolongée (M(t) × ρ_last — une estimation). Deux traits + // distincts (LABX en atténué) préservent l'honnêteté de l'affichage. + // ⚠️ Les DEUX clés doivent être SKIPPÉES par la boucle de légende + // (leçon #63 Android : sinon le catchall imprime la légende TFS en + // doublon) et avoir leurs entrées dédiées plus bas. if (state.showLabTrack) { const step = stepForRange(state.rangeHours); - const anchored = computeLabAnchoredCurve( + const labCurve = computeLabAnchoredCurve( treatments, doseLogs, labResults, startMs, endMs, step, null, + state.showLabTrackExtend, ); - curves.push(['LAB', anchored.points]); + if (state.showLabTrackExtend && labCurve.lastAnchorMs !== null) { + const anchored = labCurve.points.filter((p) => p.timestamp <= labCurve.lastAnchorMs); + const extended = labCurve.points.filter((p) => p.timestamp > labCurve.lastAnchorMs); + curves.push(['LAB', anchored]); + if (extended.length > 0) curves.push(['LABX', extended]); + } else { + curves.push(['LAB', labCurve.points]); + } } if (state.showEse || state.showTfs || state.showWhs) { // Home utilise le modèle stocké ; le graphique calibre chaque modèle @@ -288,6 +316,18 @@ export function renderChart(container, ctx) { // (série E2-only) et hors pics/creux (showExtrema=false) return { points: pts, e2Style: { color: COLORS.chartLabTrajectory, dashed: true }, tStyle: null, showExtrema: false }; } + if (model === 'LABX') { + // v1.6.0 : partie PROLONGÉE — même couleur ATTÉNUÉE (alpha 0,55, + // = #c2185b en rgba) pour distinguer l'estimation non ancrée de + // la partie qui passe par les labs. Mêmes exclusions (pas de T, + // pas d'extrema). + return { + points: pts, + e2Style: { color: 'rgba(194,24,91,0.55)', dashed: true }, + tStyle: null, + showExtrema: false, + }; + } const st = seriesStyles(model); return { points: pts, e2Style: st.e2, tStyle: st.t }; }), @@ -310,9 +350,10 @@ export function renderChart(container, ctx) { // Légende dynamique (miroir du bloc Compose) for (const [model] of curves) { - // ⚠️ La clé 'LAB' est SKIPPÉE (légende dédiée plus bas, fix #63 - // Android-miroir : sinon le `else` imprime la légende TFS en doublon) - if (model === 'LAB') continue; + // ⚠️ Les clés 'LAB' (v1.5.0) et 'LABX' (v1.6.0) sont SKIPPÉES + // (légendes dédiées plus bas, fix #63 Android-miroir : sinon le + // catchall imprime la légende TFS en doublon) + if (model === 'LAB' || model === 'LABX') continue; const st = seriesStyles(model); if (model === 'ESE') { legend.appendChild(legendLine(t('legend_ese_e2'), st.e2.color)); @@ -335,6 +376,16 @@ export function renderChart(container, ctx) { if (state.showLabTrack && curves.some(([m, pts]) => m === 'LAB' && pts.length >= 2)) { legend.appendChild(legendLine(t('legend_lab_track'), COLORS.chartLabTrajectory)); } + // v1.6.0 : légende de la partie PROLONGÉE + AVERTISSEMENT — seulement + // si la série "LABX" existe (chip Prolonger actif ET horizon atteint + // dans la fenêtre visible). Même condition pour les deux (factorisée) : + // il ne peut jamais y avoir de courbe prolongée sans son avertissement. + const labExtendVisible = state.showLabTrack && state.showLabTrackExtend + && curves.some(([m, pts]) => m === 'LABX' && pts.length >= 2); + if (labExtendVisible) { + legend.appendChild(legendLine(t('legend_lab_track_extend'), 'rgba(194,24,91,0.55)')); + legend.appendChild(el('div', { class: 'legend-muted' }, t('lab_track_extend_warning'))); + } legend.appendChild(el('div', { class: 'legend-muted' }, t('drag_hint'))); } diff --git a/js/ui/settings.js b/js/ui/settings.js index e5ab7a1..709d416 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.5.0'; +export const WEB_VERSION = '1.6.0'; /** Parse un champ numérique FR (virgule → point) ; null si vide/invalide. */ function parseNumOrNull(text) { diff --git a/js/util/i18n.js b/js/util/i18n.js index 9fadca8..550272a 100644 --- a/js/util/i18n.js +++ b/js/util/i18n.js @@ -182,6 +182,10 @@ const EN = { show_forecast: 'Forecast', show_extrema: 'Peaks & troughs', show_lab_track: 'Lab track', + // v1.6.0 : prolongation du tracé labs au-delà du dernier lab (opt-in, + // actif seulement quand Lab track est ON — miroir strings.xml Android) + show_lab_track_extend: 'Extend', + lab_track_extend_warning: '⚠ Simulation only, with no guarantee of matching reality: the extension extrapolates your model from your lab results — which can themselves be inaccurate. Trust your next blood test, not this curve.', back_to_present: 'Back to present', legend_ese_e2: 'E2 — Estrannaise (blue)', legend_ese_t: 'T — Estrannaise (pink)', @@ -193,6 +197,8 @@ const EN = { legend_forecast_dose: 'Dashed line + ▼ = projected dose', legend_extrema: '▲▼ = local peaks & troughs (with estimated values)', legend_lab_track: 'Lab-anchored curve — model shape forced through your labs (E2 only, between first and last lab)', + // v1.6.0 : légende de la partie PROLONGÉE du tracé labs (série "LABX") + legend_lab_track_extend: '-- Lab track extended past your last lab (model × last lab ratio — estimate, not anchored)', drag_hint: 'Drag horizontally to pan (right = past, left = future with forecast). Pinch or − / + to zoom (6 h → 300 d).', // Rappels (bannière + notification) reminder_notif_title: 'HormoneTrack — dose reminder', @@ -360,6 +366,10 @@ const FR = { show_forecast: 'Prévision', show_extrema: 'Pics & creux', show_lab_track: 'Tracé labs', + // v1.6.0 : prolongation du tracé labs au-delà du dernier lab (opt-in, + // actif seulement quand Tracé labs est ON — miroir strings.xml Android) + show_lab_track_extend: 'Prolonger', + lab_track_extend_warning: '⚠ Simple simulation, sans garantie de correspondre au réel : la prolongation extrapole ton modèle à partir de tes résultats de laboratoire — qui peuvent eux-mêmes être erronés. Fie-toi à ta prochaine prise de sang, pas à cette courbe.', back_to_present: 'Revenir à maintenant', legend_ese_e2: 'E2 — Estrannaise (bleu)', legend_ese_t: 'T — Estrannaise (rose)', @@ -371,6 +381,8 @@ const FR = { legend_forecast_dose: 'Ligne pointillée + ▼ = dose projetée', legend_extrema: '▲▼ = pics/creux locaux (avec valeurs estimées)', legend_lab_track: 'E2 ancrée sur tes labs — forme du modèle forcée de passer par chaque prise de sang (entre le 1er et le dernier lab)', + // v1.6.0 : légende de la partie PROLONGÉE du tracé labs (série "LABX") + legend_lab_track_extend: '-- Tracé labs prolongé après ton dernier lab (modèle × ratio du dernier lab — estimation, plus ancrée)', drag_hint: 'Glisser horizontalement pour panoramique (droite = passé, gauche = futur avec la prévision). Pincer ou − / + pour zoomer (6 h → 300 j).', reminder_notif_title: 'HormoneTrack — rappel de dose', reminder_notif_body: '{0} · {1}{2} — prévu à {3}', diff --git a/scripts/e2e.mjs b/scripts/e2e.mjs index c1cfcdc..d8f9019 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.5.0'), 'dialog : section de la version courante'); + ok(dlgText.includes('Version 1.6.0'), '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'); @@ -167,6 +167,57 @@ async function main() { ok(legendText.includes('Glisser horizontalement'), 'hint panoramique'); await page.screenshot({ path: join(SHOTS, 'e2e-chart.png') }); + // ── 2.bis Tracé labs PROLONGÉ (v1.6.0) : chips, légende, avertissement ── + // La démo place le dernier lab à now − 7 j : on dézoome (24 h → 32 j) + // pour que la partie ANCRÉE soit dans la fenêtre, puis on active + // « Tracé labs » puis « Prolonger ». ⚠️ Chaque interaction re-rend le + // DOM (redraw) → re-chercher le chip avant CHAQUE clic. ⚠️ Les chips + // filterChip sont des (onclick inline), PAS des + //