diff --git a/README.md b/README.md index acfefa7..ef4b455 100644 --- a/README.md +++ b/README.md @@ -17,12 +17,12 @@ > Fie-toi toujours à tes analyses de sang et aux consignes de ton > endocrinologue. -- **Statut** : web v1.9.5 — parité fonctionnelle avec l'Android v1.9.5 +- **Statut** : web v1.9.6 — parité fonctionnelle avec l'Android v1.9.6 (modèle Estrannaise analytique + nuage d'incertitude MCMC exclusif ESE ; 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 + **178 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 fad4117..2a0aeb9 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -5,6 +5,20 @@ > 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.6] — 2026-09-20 + +### Modifié — la carte de l'accueil liste TOUTES les prochaines prises + +- Miroir de l'Android v1.9.6 (remontée : le rappel quotidien CPA masquait + le rappel hebdomadaire EEn — « je ne le vois quasiment pas ») : la carte + « Prochaine dose » devient « **Prochaines doses** » (titre pluriel dès 2 + lignes) — **une ligne par traitement, TRIÉES par prochaine prise** ; + la 1ʳᵉ ligne (la plus proche) reste mise en avant. Format de ligne + inchangé (`Xj Yh · sam. 26 10:00 (nom)`, `XhMM · HH:mm`, `N min`). +- Moteur : helper PUR `nextDoseLineText` dans `format.js` (miroir du + Kotlin `nextDoseLine`) — 3 tests miroirs dans misc.test.js. +- Les NOTIFICATIONS de rappel sont inchangées. + ## [1.9.5] — 2026-09-20 ### Corrigé — revue de code post-v1.9.4 (audit lecture-seule des deux dépôts) diff --git a/js/ui/home.js b/js/ui/home.js index 2584654..7ea647b 100644 --- a/js/ui/home.js +++ b/js/ui/home.js @@ -21,7 +21,7 @@ import { computeCurve, autoCalibrated, nextReminderFireFor, Alerts, TConfig, pointHoursBefore, nextBloodDrawRecommendation } from '../pk/index.js'; import { store } from '../data/store.js'; import { t } from '../util/i18n.js'; -import { formatDose, formatValue, formatTime, formatWeekdayTime, formatDateTime, HrtDuration } from '../util/format.js'; +import { formatDose, formatValue, formatDateTime, formatWeekdayTime, nextDoseLineText } from '../util/format.js'; import { el } from './components.js'; import { drawChart, COLORS } from './chart-canvas.js'; import { openDoseDialog } from './dialogs.js'; @@ -109,7 +109,14 @@ export function renderHome(container, ctx) { } } - // ── Carte « prochaine dose » (grille Posologie via le moteur, fix #52) ─── + // ── Carte « prochaines doses » (grille Posologie via le moteur, fix #52) ── + // v1.9.6 (demande) : la carte liste TOUTES les prochaines prises, UNE + // ligne par traitement TRIÉES par prochaine prise (helper pur + // nextDoseLineText, miroir du Kotlin nextDoseLine) — l'ancienne carte ne + // montrait que la plus proche, et une prise quotidienne (CPA) masquait + // totalement le rappel de l'injection hebdomadaire. Le titre passe au + // pluriel dès qu'il y a au moins deux lignes ; la 1ʳᵉ ligne (la plus + // proche) reste mise en avant (font-weight 600). const fires = []; for (const tr of treatments) { const fire = nextReminderFireFor(tr, doseLogs, nowMs); @@ -117,24 +124,12 @@ export function renderHome(container, ctx) { } if (fires.length) { fires.sort((a, b) => a[1] - b[1]); - const [nextTreatment, next] = fires[0]; - const deltaMs = next - nowMs; - const h = Math.floor(deltaMs / HOUR_MS); - const m = Math.floor((deltaMs % HOUR_MS) / 60000); const card = el('div', { class: 'card' }); - card.appendChild(el('div', { class: 'label' }, t('next_dose'))); - if (h >= 24) { - // v1.4.1 : au-delà de 24 h, afficher en JOURS + jour de semaine - const [days, remH] = HrtDuration.daysAndHours(h); - card.appendChild(el('div', { style: 'font-weight:600;' }, - t('next_dose_days', days, remH, formatWeekdayTime(next), nextTreatment.name))); - } else if (h > 0) { - card.appendChild(el('div', { style: 'font-weight:600;' }, - `${h}h${String(m).padStart(2, '0')} · ${formatTime(next)} (${nextTreatment.name})`)); - } else { - card.appendChild(el('div', { style: 'font-weight:600;' }, - `${m} min · ${formatTime(next)} (${nextTreatment.name})`)); - } + card.appendChild(el('div', { class: 'label' }, t(fires.length === 1 ? 'next_dose' : 'next_doses'))); + fires.forEach(([treatment, next], idx) => { + card.appendChild(el('div', { style: idx === 0 ? 'font-weight:600;' : '' }, + nextDoseLineText(nowMs, next, treatment.name))); + }); col.appendChild(card); } diff --git a/js/ui/settings.js b/js/ui/settings.js index c0f6d45..22237a5 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.5'; +export const WEB_VERSION = '1.9.6'; /** Parse un champ numérique FR (virgule → point) ; null si vide/invalide. */ function parseNumOrNull(text) { diff --git a/js/util/format.js b/js/util/format.js index 88f4bfb..d3b3ddb 100644 --- a/js/util/format.js +++ b/js/util/format.js @@ -113,6 +113,33 @@ export function formatLongDate(ms, timeZone = null) { }), timeZone).format(new Date(ms)); } +/** + * Ligne « prochaine prise » d'un traitement (v1.9.6 — miroir du helper + * `nextDoseLine` Kotlin) : « Xj Yh · sam. 26 10:00 (nom) » au-delà de 24 h + * (v1.4.1 : les heures deviennent peu lisibles), « XhMM · HH:mm (nom) » en + * dessous, « N min · HH:mm (nom) » dans l'heure. PUR, testé dans + * tests/misc.test.js — sert à CHAQUE ligne de la carte « Prochaines doses » + * de l'accueil (une ligne par traitement, triées par prochaine prise). + * + * @param {number} nowMs maintenant + * @param {number} fireMs prochaine prise du traitement + * @param {string} name nom du traitement + * @param {string} [timeZone=null] + * @returns {string} + */ +export function nextDoseLineText(nowMs, fireMs, name, timeZone = null) { + const deltaMs = fireMs - nowMs; + const h = Math.floor(deltaMs / 3600000); + const m = Math.floor((deltaMs % 3600000) / 60000); + if (h >= 24) { + const days = Math.floor(h / 24); + const remH = h % 24; + return `${days}j ${remH}h · ${formatWeekdayTime(fireMs, timeZone)} (${name})`; + } + if (h > 0) return `${h}h${String(m).padStart(2, '0')} · ${formatTime(fireMs, timeZone)} (${name})`; + return `${m} min · ${formatTime(fireMs, timeZone)} (${name})`; +} + /** * « EEE d HH:mm » (carte prochaine dose au-delà de 24 h — jour de semaine * inclus, cf v1.4.1). diff --git a/js/util/i18n.js b/js/util/i18n.js index 81a51f4..572897c 100644 --- a/js/util/i18n.js +++ b/js/util/i18n.js @@ -42,6 +42,7 @@ const EN = { no_data_short: 'No data — log an injection to see estimates', delta_6h: '{0} {1} pg/mL vs 6 h ago', next_dose: 'Next dose', + next_doses: 'Upcoming doses', next_dose_days: '{0} d {1} h · {2} ({3})', logged_today: 'Quick log', home_chart_title: 'Last 24 h', @@ -252,6 +253,7 @@ const FR = { no_data_short: 'Aucune donnée — logue une injection pour voir les estimations', delta_6h: '{0} {1} pg/mL vs il y a 6 h', next_dose: 'Prochaine dose', + next_doses: 'Prochaines doses', next_dose_days: '{0} j {1} h · {2} ({3})', logged_today: 'Log rapide', home_chart_title: 'Dernières 24 h', diff --git a/tests/misc.test.js b/tests/misc.test.js index 0b41635..06fe9f2 100644 --- a/tests/misc.test.js +++ b/tests/misc.test.js @@ -14,7 +14,7 @@ import { nextReminderFireFor, nextReminderFireMs } from '../js/pk/pk-reminders.j import { detectExtrema } from '../js/pk/pk-extrema.js'; import { LevelPoint } from '../js/pk/pk-engine.js'; import { isVersionNewer, sectionsSince } from '../js/util/changelog.js'; -import { HrtDuration } from '../js/util/format.js'; +import { HrtDuration, nextDoseLineText } from '../js/util/format.js'; import { makeTreatment, makeDose, DAY_MS, HOUR_MS, assertClose } from './helpers.js'; /** Met (h, m) sur le jour calendaire de [ms] (locale — miroir du module testé). */ @@ -212,3 +212,30 @@ describe('HrtDuration (temps sous THS)', () => { assert.throws(() => assertClose(300, 100, 0.02)); }); }); + +describe('nextDoseLineText (carte « Prochaines doses » — v1.9.6, miroir NextDoseLineTest.kt)', () => { + // Hors DST : les heures attendues sont construites avec les MÊMES + // primitives (Intl fuseau système) que le module testé. + const now = new Date(2026, 8, 21, 12, 0, 0).getTime(); // lundi 21/09/2026 12:00 + const timeOf = (h, m) => new Date(2026, 8, 21, h, m, 0).getTime(); + + test('au-delà de 24 h — jours + jour de semaine', () => { + // fire = samedi 26/09 10:00 (118 h plus tard) + const fire = now + 118 * HOUR_MS; + const line = nextDoseLineText(now, fire, 'Injection EEn 5mg 7d'); + assert.match(line, /^4j 22h · .+ \(Injection EEn 5mg 7d\)$/); + // le jour de semaine est présent (le format Intl diffère selon la locale + // système : on épingle la STRUCTURE, pas le libellé) + assert.ok(!line.startsWith('118'), 'delta affiché en jours, pas en heures'); + }); + + test('entre 1 h et 24 h — heures + minutes', () => { + const fire = timeOf(14, 5); + assert.equal(nextDoseLineText(now, fire, 'CPA'), `2h05 · 14:05 (CPA)`); + }); + + test('moins d\'une heure — minutes', () => { + const fire = timeOf(12, 7); + assert.equal(nextDoseLineText(now, fire, 'CPA'), `7 min · 12:07 (CPA)`); + }); +});