v1.6.1 : fix — les options du graphique se réinitialisaient toutes seules
Cause (divergence de portage, bug remonté « options reset sans changer de
menu ») : renderRoute() re-crée l'écran ENTIER à chaque tick minute (60 s),
mutation du store ou resize — et l'état des options était une variable
LOCALE de renderChart, retombant aux défauts à chaque re-création.
L'Android est immunisé (remember { mutableStateOf } survit aux
recompositions).
Fix (miroir Compose) :
- chart.js : état d'interaction au NIVEAU MODULE (chartUiState) ;
renderChart(container, ctx, { preserveState }) réutilise l'état quand
preserveState=true, le recrée aux défauts sinon ;
- app.js : preserveChartState = (route === 'chart' && lastRoute === 'chart')
comparé AVANT la mise à jour de lastRoute — tick/store/resize préservent
l'état, la navigation le réinitialise (miroir du reset d'un remember
Android quitté).
Épinglé par l'E2E : resize (même code path que le tick) → chip Tracé labs
+ légende restent en place ; navigation → retour aux défauts.
Docs : §4 « État d'interaction persistant », §8 scénario E2E, §11 leçon
#15 (tout état d'UI doit survivre au re-rendu global), CHANGELOG [1.6.1]
(fix web-only : parité fonctionnelle = Android 1.6.0), README.
This commit is contained in:
parent
ed623b786b
commit
0d6161b588
@ -17,8 +17,10 @@
|
||||
> Fie-toi toujours à tes analyses de sang et aux consignes de ton
|
||||
> endocrinologue.
|
||||
|
||||
- **Statut** : web v1.6.0 — portage de l'Android v1.6.0 · **140 tests verts** ·
|
||||
E2E navigateur ✅ · conteneur testé en configuration durcie ✅ · lint/i18n ✅
|
||||
- **Statut** : web v1.6.1 — parité fonctionnelle avec l'Android v1.6.0
|
||||
(+ fix web-only : options du graphique préservées entre re-rendus) ·
|
||||
**140 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)
|
||||
(zip de déploiement statique vérifié par téléchargement)
|
||||
|
||||
@ -5,6 +5,31 @@
|
||||
> 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.6.1] — 2026-09-13
|
||||
|
||||
### Corrigé — les options du graphique se réinitialisaient « toutes seules »
|
||||
|
||||
- **Bug remonté** : sur l'écran Graphiques, les options sélectionnées
|
||||
(modèles affichés, Tracé labs, Prolonger, zoom, panoramique…) revenaient
|
||||
d'elles-mêmes aux défauts « alors qu'on n'a pas changé de menu ».
|
||||
- **Cause** (divergence de portage) : le shell web re-crée l'écran ENTIER
|
||||
à chaque **tick minute** (60 s), **mutation du store** ou **resize** —
|
||||
et l'état des options vivait dans une variable LOCALE de la fonction de
|
||||
rendu, donc retombait aux défauts à chaque re-crétion. Côté Android,
|
||||
l'équivalent vit dans des `remember { mutableStateOf }` qui survivent à
|
||||
toutes les recompositions : le bug est structurellement impossible côté
|
||||
APK — c'était un piège du portage, pas de la logique.
|
||||
- **Fix** : l'état d'interaction du graphique vit au niveau MODULE et le
|
||||
shell signale les re-rendus du **même** écran (`preserveState: true`)
|
||||
→ options préservées pendant qu'on regarde le graphique ; à l'entrée
|
||||
depuis la navigation (quitter puis revenir), retour aux défauts —
|
||||
comportement identique à l'Android.
|
||||
- **Épinglé par l'E2E** : re-render déclenché (resize — même code path
|
||||
que le tick) → chip et légende toujours en place ; navigation → reset.
|
||||
|
||||
> Note de parité : fix **web-only** (pas de changement Android) — les
|
||||
> fonctionnalités portées restent celles de l'Android v1.6.0.
|
||||
|
||||
## [1.6.0] — 2026-09-12
|
||||
|
||||
### « Tracé labs » prolongé au-delà du dernier lab — portage de l'Android v1.6.0
|
||||
|
||||
@ -272,6 +272,16 @@ HormoneTrack-web/ (dépôt séparé)
|
||||
- **« now » mémoïsé sur un tick minute** (fix #56 transposé) : `ctx.nowMs`
|
||||
est fourni par le shell et ne change qu'au re-rendu (tick 60 s, mutation
|
||||
du store, navigation) — jamais de dérive de fenêtre pendant les gestes.
|
||||
- **État d'interaction du graphique persistant entre re-rendus** (v1.6.1,
|
||||
fix du bug « options qui se reset toutes seules ») : `renderRoute()`
|
||||
re-crée l'écran ENTIER à chaque tick minute (60 s), mutation du store ou
|
||||
resize — or l'Android garde les options dans des `remember` qui
|
||||
SURVIVENT aux recompositions. Le web fait donc vivre l'état AU NIVEAU
|
||||
MODULE (`chartUiState` dans chart.js) et le shell signale les re-rendus
|
||||
du même écran via `preserveState: true` (tick/store/resize → préservé ;
|
||||
navigation → défauts, miroir du reset d'un `remember` quitté). Sans ça,
|
||||
les options du graphique retombaient aux défauts toutes les 60 s
|
||||
pendant que l'utilisateur le regarde.
|
||||
- **Le graphique est purement déclaratif** : pan/zoom vivent dans
|
||||
`chart.js` (parent), `chart-canvas.js` ne fait que dessiner une fenêtre
|
||||
donnée — même séparation que Compose (piège #62/§11 Android).
|
||||
@ -455,6 +465,13 @@ launch) — d'où le build dédié dans le cache Playwright, hors dépôt.
|
||||
ancrée, la légende prolongée et l'**AVERTISSEMENT simulation** (sans
|
||||
garantie, labs potentiellement erronés) — puis désactive et vérifie
|
||||
qu'ils disparaissent ;
|
||||
- **v1.6.1 : scénario « persistance des options »** — avec `Tracé labs`
|
||||
activé, déclenche un re-render via `dispatchEvent(new Event('resize'))`
|
||||
(MÊME code path que le tick minute et la mutation du store : tous trois
|
||||
appellent `renderRoute` — le tick de 60 s est trop lent pour un E2E) et
|
||||
vérifie que le chip reste sélectionné ET la légende affichée ; puis
|
||||
navigation (quitter → revenir) et vérifie le RESET aux défauts (miroir
|
||||
du `remember` Android quitté).
|
||||
- vérifie l'**absence d'erreur console/page** sur toute la session ;
|
||||
- captures dans `/tmp/hrt-web-shots/` (inspection visuelle).
|
||||
- ⚠️ Course au screenshot évitée par le marqueur `body[data-ready="1"]`
|
||||
@ -710,6 +727,17 @@ Trouvés pendant l'écriture (à ne pas réintroduire) :
|
||||
d'où la garde `check.sh` §3.bis : quand le dépôt Android voisin
|
||||
existe, les symboles clés des features partagées sont vérifiés des
|
||||
DEUX côtés (moteur + chip + avertissement).
|
||||
15. **Re-rendu global = tout état d'UI doit survivre** (v1.6.1, bug
|
||||
remonté : « les options du graphique se reset alors qu'on n'a pas
|
||||
changé de menu ») : le shell re-crée l'écran ENTIER sur tick minute,
|
||||
mutation du store et resize (3 déclencheurs, `renderRoute`) — un
|
||||
état local à une fonction de rendu retombe donc aux défauts toutes
|
||||
les 60 s SANS action de l'utilisateur. Règle : tout état d'interaction
|
||||
d'un écran vit au niveau MODULE du fichier UI, et `renderRoute`
|
||||
transmet « même écran re-rendu » (`preserveState`) pour le préserver
|
||||
— miroir de `remember` Compose (survit aux recompositions, reset à la
|
||||
navigation). Côté Android ce bug est structurellement impossible
|
||||
(Compose garde l'état) : c'est un piège de portage, pas de logique.
|
||||
|
||||
## 12. Limites connues & non-portés
|
||||
|
||||
|
||||
13
js/ui/app.js
13
js/ui/app.js
@ -141,6 +141,17 @@ function currentRoute() {
|
||||
function renderRoute() {
|
||||
if (!contentEl) return;
|
||||
const route = currentRoute();
|
||||
// (v1.6.1) Re-rendu du MÊME écran graphique (tick minute, mutation du
|
||||
// store, resize — ce re-rendu ne vient PAS de la navigation) ? Le chart
|
||||
// doit alors PRÉSERVER son état d'interaction (chips, zoom, pan,
|
||||
// Tracé labs…) : miroir Compose où `remember` survit aux recompositions.
|
||||
// Sans ça, le tick minute de 60 s réinitialisait toutes les options du
|
||||
// graphique pendant que l'utilisateur le regarde (bug remonté : « les
|
||||
// options se reset alors qu'on n'a pas changé de menu »). À l'ENTRÉE
|
||||
// (navigation depuis un autre onglet) : preserveState = false → défauts,
|
||||
// miroir du reset Android quand on quitte l'écran. ⚠️ comparé AVANT la
|
||||
// mise à jour de lastRoute ci-dessous.
|
||||
const preserveChartState = route === 'chart' && lastRoute === 'chart';
|
||||
lastRoute = route;
|
||||
|
||||
// Écran de chargement tant que l'asset PK n'est pas chargé (les écrans
|
||||
@ -184,7 +195,7 @@ function renderRoute() {
|
||||
renderHome(contentEl, { nowMs, openChart: () => { location.hash = '#chart'; } });
|
||||
break;
|
||||
case route === 'chart':
|
||||
renderChart(contentEl, { nowMs });
|
||||
renderChart(contentEl, { nowMs }, { preserveState: preserveChartState });
|
||||
break;
|
||||
case route === 'doses':
|
||||
renderDoses(contentEl);
|
||||
|
||||
@ -19,6 +19,11 @@
|
||||
* - « now » mémoïsé sur un tick minute (fix #56 : boucle de recomposition
|
||||
* côté Android — côté web, même discipline : endMs ne dérive pas à chaque
|
||||
* frame, il ne change qu'au tick ou sur interaction) ;
|
||||
* - ÉTAT D'INTERACTION PERSISTANT entre re-rendus (v1.6.1) : le tick minute
|
||||
* de app.js, une mutation du store ou un resize re-créent l'écran entier —
|
||||
* l'état (chips, zoom, pan, Tracé labs, Prolonger…) est réutilisé quand
|
||||
* on re-rend le même écran (miroir `remember` Compose) et réinitialisé
|
||||
* à l'entrée depuis la navigation (miroir du reset Android) ;
|
||||
* - PAS de scroll horizontal parent : le drag horizontal est réservé au pan
|
||||
* (piège Android #62/§11).
|
||||
* ─────────────────────────────────────────────────────────────────────────────
|
||||
@ -43,6 +48,29 @@ const HOUR_MS = 3600000;
|
||||
const MIN_RANGE_H = 6;
|
||||
const MAX_RANGE_H = 24 * 300;
|
||||
|
||||
// ── État d'interaction du graphique (v1.6.1 — fix du bug « options qui se
|
||||
// reset toutes seules ») ──────────────────────────────────────────────────────
|
||||
// ⚠️ DIVERGENCE DE PORTAGE corrigée : côté Android, les options du graphique
|
||||
// vivent dans des `remember { mutableStateOf }` — elles SURVIVENT à toutes
|
||||
// les recompositions (tick minute, nouvelles données, resize) tant qu'on
|
||||
// reste sur l'écran. Côté web, `renderRoute()` (app.js) re-crée l'écran
|
||||
// ENTIER à chaque tick minute (60 s), mutation du store ou resize — et un
|
||||
// `const state = {...}` local à renderChart retombait donc aux DÉFAUTS
|
||||
// toutes les 60 s pendant qu'on regarde le graphique (remontée : « les
|
||||
// options se réinitialisent sans qu'on change de menu »).
|
||||
//
|
||||
// FIX : l'état vit AU NIVEAU MODULE. `renderChart` le réutilise quand
|
||||
// l'appelant signale un re-rendu DU MÊME écran (`preserveState: true` —
|
||||
// miroir de `remember` qui survit aux recompositions) et le RECRÉE aux
|
||||
// défauts à l'ENTRÉE sur l'écran (navigation depuis un autre onglet —
|
||||
// miroir du reset d'un `remember` quitté, PAS un rememberSaveable :
|
||||
// quitter Charts puis revenir réinitialise, comme l'Android).
|
||||
//
|
||||
// Un seul objet à la fois : le graphique est un écran unique, le DOM
|
||||
// précédent est jeté à chaque re-rendu (les closures recaptent `state`,
|
||||
// qui reste le MÊME objet d'un re-rendu à l'autre quand il est préservé).
|
||||
let chartUiState = null;
|
||||
|
||||
/** Styles visuels des séries (miroir de la map du ChartScreen Android). */
|
||||
function seriesStyles(model) {
|
||||
if (model === 'ESE') return { e2: { color: COLORS.chartE2, dashed: false }, t: { color: COLORS.chartT, dashed: false } };
|
||||
@ -52,10 +80,19 @@ function seriesStyles(model) {
|
||||
|
||||
/**
|
||||
* Rend l'écran Graphiques dans [container].
|
||||
*
|
||||
* @param {HTMLElement} container
|
||||
* @param {object} ctx { nowMs }
|
||||
* @param {object} [opts]
|
||||
* @param {boolean} [opts.preserveState=false] (v1.6.1) `true` quand on
|
||||
* re-rend le MÊME écran graphique (tick minute, mutation du store,
|
||||
* resize — cf app.js renderRoute) : l'état d'interaction [state]
|
||||
* est réutilisé au lieu d'être recréé aux défauts (miroir Compose :
|
||||
* `remember` survit aux recompositions). `false` = entrée sur
|
||||
* l'écran (navigation) : état neuf aux défauts (miroir du reset
|
||||
* d'un `remember` Android quand on quitte l'écran).
|
||||
*/
|
||||
export function renderChart(container, ctx) {
|
||||
export function renderChart(container, ctx, { preserveState = false } = {}) {
|
||||
const treatments = store.getTreatments(); // TOUS (actifs ET inactifs, §6.bis)
|
||||
const doseLogs = store.getDoseLogs();
|
||||
const labResults = store.getLabResults();
|
||||
@ -65,27 +102,33 @@ export function renderChart(container, ctx) {
|
||||
const chartTimeZone = settings.chartTimezone || null;
|
||||
|
||||
// ── État d'interaction (miroir des mutableStateOf Compose) ────────────────
|
||||
const state = {
|
||||
rangeHours: 24,
|
||||
panHours: 0,
|
||||
showT: true,
|
||||
showLabs: true,
|
||||
showEse: true,
|
||||
showTfs: true,
|
||||
showWhs: false,
|
||||
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,
|
||||
};
|
||||
// v1.6.1 : réutilisé entre les re-rendus du même écran (cf bloc
|
||||
// « État d'interaction » au-dessus — sans ça, le tick minute de app.js
|
||||
// réinitialisait toutes les options toutes les 60 s).
|
||||
if (!preserveState || !chartUiState) {
|
||||
chartUiState = {
|
||||
rangeHours: 24,
|
||||
panHours: 0,
|
||||
showT: true,
|
||||
showLabs: true,
|
||||
showEse: true,
|
||||
showTfs: true,
|
||||
showWhs: false,
|
||||
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,
|
||||
};
|
||||
}
|
||||
const state = chartUiState;
|
||||
|
||||
// ── Colonne racine ───────────────────────────────────────────────────────
|
||||
const col = el('div', {});
|
||||
|
||||
@ -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.6.0';
|
||||
export const WEB_VERSION = '1.6.1';
|
||||
|
||||
/** Parse un champ numérique FR (virgule → point) ; null si vide/invalide. */
|
||||
function parseNumOrNull(text) {
|
||||
|
||||
@ -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.6.0'), 'dialog : section de la version courante');
|
||||
ok(dlgText.includes('Version 1.6.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');
|
||||
@ -218,6 +218,45 @@ async function main() {
|
||||
'désactivation → légende prolongée + avertissement disparaissent');
|
||||
await page.screenshot({ path: join(SHOTS, 'e2e-chart-extend.png') });
|
||||
|
||||
// ── 2.ter PERSISTANCE de l'état du graphique (v1.6.1) ───────────────────
|
||||
// Bug remonté : les options se réinitialisaient « toutes seules » car le
|
||||
// tick minute / une mutation du store / un resize re-créaient l'écran
|
||||
// ENTIER (renderRoute) et le state local de renderChart retombait aux
|
||||
// défauts. Fix : état au niveau module + preserveState du même écran.
|
||||
// Ici on déclenche le re-render via RESIZE (même code path que le tick :
|
||||
// store.subscribe et setInterval appellent tous renderRoute) — le tick
|
||||
// réel de 60 s est trop lent pour un E2E.
|
||||
console.log('── Persistance des options du graphique (#chart, v1.6.1) ──');
|
||||
// « Tracé labs » est ON à ce stade (scénario 2.bis) — vérifions avant :
|
||||
const selectedBefore = await page.evaluate(() =>
|
||||
[...document.querySelectorAll('.chip')]
|
||||
.some((x) => x.textContent.trim() === 'Tracé labs' && x.classList.contains('selected')));
|
||||
ok(selectedBefore, 'pré-état : chip « Tracé labs » activé');
|
||||
await page.evaluate(() => window.dispatchEvent(new Event('resize')));
|
||||
await page.waitForTimeout(500);
|
||||
const stateAfterResize = await page.evaluate(() => ({
|
||||
labTrackSelected: [...document.querySelectorAll('.chip')]
|
||||
.some((x) => x.textContent.trim() === 'Tracé labs' && x.classList.contains('selected')),
|
||||
legend: document.querySelector('main.content').textContent.includes('E2 ancrée sur tes labs'),
|
||||
}));
|
||||
ok(stateAfterResize.labTrackSelected,
|
||||
'resize → le chip « Tracé labs » reste sélectionné (état préservé, bug v1.6.1 corrigé)');
|
||||
ok(stateAfterResize.legend,
|
||||
'resize → la légende « Tracé labs » reste affichée (le re-rendu n\'a pas tout remis à zéro)');
|
||||
// Reset à l'ENTRÉE (navigation) : quitter puis revenir → défauts,
|
||||
// miroir du reset d'un `remember` Android quand on quitte l'écran
|
||||
await page.goto(`${base}/#home`, { waitUntil: 'load' });
|
||||
await page.waitForSelector('body[data-ready="1"]');
|
||||
await page.goto(`${base}/#chart`, { waitUntil: 'load' });
|
||||
await page.waitForSelector('body[data-ready="1"]');
|
||||
await page.waitForTimeout(400);
|
||||
const stateAfterNav = await page.evaluate(() => ({
|
||||
labTrackSelected: [...document.querySelectorAll('.chip')]
|
||||
.some((x) => x.textContent.trim() === 'Tracé labs' && x.classList.contains('selected')),
|
||||
}));
|
||||
ok(stateAfterNav.labTrackSelected === false,
|
||||
'navigation (quitter → revenir) → options retombées aux défauts (miroir Android)');
|
||||
|
||||
// ── 3. Navigation : les 5 onglets + settings ────────────────────────────
|
||||
console.log('── Navigation ──');
|
||||
await page.goto(`${base}/#doses`, { waitUntil: 'load' });
|
||||
@ -243,7 +282,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.6.0 (web)'), 'Paramètres : version affichée');
|
||||
ok(text.includes('Version 1.6.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') });
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user