v1.9.6 (web) : la carte Accueil liste TOUTES les prochaines doses (triées) — miroir Android

- helper PUR nextDoseLineText dans format.js (miroir du Kotlin
  nextDoseLine) : « Xj Yh · sam. 26 10:00 (nom) » > 24 h, « XhMM » sinon,
  « N min » dans l'heure
- home.js : une ligne par traitement, triées par prochaine prise, 1ʳᵉ en
  avant, titre pluriel « Prochaines doses » dès 2 lignes (i18n EN/FR)
- 3 tests miroirs dans misc.test.js (178 verts + E2E)
- WEB_VERSION 1.9.6 + CHANGELOG/README
This commit is contained in:
Siphonight 2026-09-21 21:57:04 +02:00
parent be4d26b3e5
commit 76a49175c5
7 changed files with 88 additions and 23 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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);
}

View File

@ -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) {

View File

@ -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).

View File

@ -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',

View File

@ -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)`);
});
});