- presets.js : EC huile (5 mg IM), EB (1 mg IM), EUCS suspension cristalline (10 mg SC) — 25 entrées, champs Bateman informatifs - pk-engine.js : Esters.EUCS manquant (portage v1.9.0 incomplet — le preset référençait undefined) - models.js : choicesForModel ESE = 6 esters (parité Android v1.9.0 ; était resté sur la couverture des tables ODS à 3) + KDoc à jour - treatment-editor.js : liste des esters = choicesForModel(model) — rebascule sur EV si l'ester sort de la couverture au changement de modèle (garde anti 0 pg/mL) + re-render du formulaire - tests/presets.test.js (nouveau, 6) : couverture 6 esters ESE, unicité, i18n EN/FR, TFS 7 / WHSAH 6 inchangés, choicesForModel aligné dispatchs - versions 1.9.4 (settings + e2e) + CHANGELOG/§4/§8/README
823 lines
49 KiB
Markdown
823 lines
49 KiB
Markdown
# Documentation de développement — HormoneTrack **Web**
|
||
|
||
> Doc de référence pour toute session (humaine ou IA) travaillant sur la
|
||
> version web : architecture, correspondances avec l'app Android, format du
|
||
> backup, tests, déploiement, pièges. Le code est commenté au même niveau que
|
||
> la doc — cette page explique le POURQUOI et le LIEN à l'Android.
|
||
>
|
||
> Projet : `~/projects/HormoneTrack-web` — **dépôt GIT SÉPARÉ** de l'app
|
||
> Android (décision v1.4.10, cf §3) ; l'app Android vit dans
|
||
> `~/projects/HormoneTrack`
|
||
> ([gitea.cloudyfy.fr/Siphonight/HormoneTrack](https://gitea.cloudyfy.fr/Siphonight/HormoneTrack),
|
||
> docs mère : son `docs/DEVELOPPEMENT.md`).
|
||
> Guide utilisateur : [../README.md](../README.md).
|
||
|
||
---
|
||
|
||
## Table des matières
|
||
|
||
1. [Contexte & objectifs du portage](#1-contexte--objectifs-du-portage)
|
||
2. [Stack & décisions structurantes](#2-stack--décisions-structurantes)
|
||
3. [Deux dépôts séparés : HormoneTrack (APK) ↔ HormoneTrack-web](#3-deux-dépôts-séparés--hormonetrack-apk--hormonetrack-web)
|
||
4. [Architecture & correspondance fichier par fichier](#4-architecture--correspondance-fichier-par-fichier)
|
||
5. [Persistance (localStorage) & schéma de données](#5-persistance-localstorage--schéma-de-données)
|
||
6. [Compatibilité des backups avec l'Android](#6-compatibilité-des-backups-avec-landroid)
|
||
7. [Fuseaux horaires (Intl)](#7-fuseaux-horaires-intl)
|
||
8. [Tests — processus complet](#8-tests--processus-complet)
|
||
9. [Workflow de développement](#9-workflow-de-développement)
|
||
10. [Processus de push & releases (sync Android)](#10-processus-de-push--releases-sync-android)
|
||
11. [Bugs potentiels évités pendant le portage](#11-bugs-potentiels-évités-pendant-le-portage)
|
||
12. [Limites connues & non-portés](#12-limites-connues--non-portés)
|
||
13. [Idées d'évolution](#13-idées-dévolution)
|
||
|
||
---
|
||
|
||
## 1. Contexte & objectifs du portage
|
||
|
||
L'utilisatrice suit son THS sur l'app Android (cf doc mère). La version web
|
||
existe pour :
|
||
|
||
- **disponibilité** : consulter ses courbes depuis n'importe quel appareil
|
||
(ordinateur, iPad) sans installation ;
|
||
- **portabilité des données** : les backups JSON de l'app Android
|
||
s'importent tels quels (et réciproquement) — pas de verrou.
|
||
|
||
Règles du portage, fixées au départ :
|
||
|
||
1. **Fidélité du moteur PK** : mêmes maths, mêmes paramètres, mêmes pins de
|
||
tests que l'Android (les courbes doivent être INDISTINGUABLES).
|
||
2. **Compatibilité backup bidirectionnelle** (schéma `BackupData` v2, champs
|
||
Gson/Kotlin identiques).
|
||
3. **100 % local** : tout dans le navigateur (localStorage), le serveur ne
|
||
sert que des fichiers statiques.
|
||
4. **Aucune dépendance applicative** : pas de framework, pas de build, pas
|
||
de node_modules à l'exécution — le dépôt est un site statique déployable
|
||
tel quel. (Les devDependencies npm ne servent QU'aux tests E2E.)
|
||
5. **Dépôt GIT SÉPARÉ** de l'app Android (décision v1.4.10, cf §3) —
|
||
changelogs, docs, historique, tags et releases totalement indépendants.
|
||
|
||
## 2. Stack & décisions structurantes
|
||
|
||
| Choix | Décision | Pourquoi |
|
||
|---|---|---|
|
||
| Langage | JavaScript ES modules natifs (pas de TS, pas de build) | zéro chaîne de compilation → le déploiement = copier le dossier ; les types critiques sont documentés en JSDoc et épinglés par 132 tests |
|
||
| Framework UI | Aucun — DOM via un helper `el()` (`ui/components.js`) | même philosophie que la v1 Android (pas de ViewModel/DI) ; une seule abstraction à connaître |
|
||
| Rendu graphique | Canvas 2D natif (portage de `CurveChart.kt`) | pas de lib de charts : fidélité du rendu, zéro dépendance |
|
||
| Persistance | `localStorage` (clés préfixées `hormonetrack.`) | données petites (JSON) ; backend INJECTABLE → testable en Node (`store.setBackend(new Map())`) |
|
||
| i18n | dictionnaires JS (`util/i18n.js`), langue persistée | miroir values/strings.xml ; `system` suit `navigator.language` |
|
||
| Fuseaux | `Intl.DateTimeFormat` (natif) | remplace `java.util.Calendar/TimeZone` — cf §7 |
|
||
| Tests | `node --test` (runner natif Node ≥ 20) + E2E playwright | zéro dépendance pour les tests unitaires ; l'E2E utilise le build Firefox de Playwright |
|
||
| Asset PK | **copie** de l'asset Android (`pk_profiles.json`) | mêmes tables que l'app Android — `tests/pk-profiles.test.js` vérifie les 6 profils et les pics ODS ; TOUTE retouche se fait dans les DEUX dépôts (règle §3) |
|
||
|
||
⚠️ **Ne pas introduire de bundler/build sans décision inverse documentée** :
|
||
le déploiement « copier un dossier » est une propriété voulue du projet.
|
||
|
||
## 3. Deux dépôts séparés : HormoneTrack (APK) ↔ HormoneTrack-web
|
||
|
||
**Décision (8 sept. 2026)** : le portage vit dans son PROPRE dépôt git,
|
||
avec ses propres remotes, historique, tags et releases — les deux apps ne
|
||
partagent plus qu'une CONVENTION de version.
|
||
|
||
### Pourquoi (motifs qui ont tranché)
|
||
|
||
1. **Confidentialité** : l'historique du dépôt Android contient une dette
|
||
connue et jugée (fragments de labs dans les révisions v1.1.0→v1.2.3, cf
|
||
§8.bis de la doc Android) — non réexportable sans re-décision. Le dépôt
|
||
web **naît avec un historique 100 % propre** : il peut devenir public un
|
||
jour sans AUCUNE re-décision, pendant que la dette reste confinée à
|
||
l'Android.
|
||
2. **Releases indépendantes** : « pas de release web pour l'instant » =
|
||
ne rien faire côté web. Les releases APK ne traînent jamais un artifact
|
||
web et inversement.
|
||
3. **Simplicité** : plus de conventions de préfixes (`web:`) ni de tags
|
||
appariés à maintenir dans un historique unique.
|
||
|
||
### Ce que ça implique (règles de croisement)
|
||
|
||
| Aspect | Règle |
|
||
|---|---|
|
||
| **Version** | `WEB_VERSION` = version ANDROID portée, **alignée à chaque release** (web v1.4.10 = portage complet de l'Android v1.4.10, sauf impossibilités structurelles documentées §12) |
|
||
| **Tags** | les DEUX dépôts tagguent `vX.Y.Z` au même rythme (dans HormoneTrack-web, `v1.4.10` = l'état web correspondant à l'Android v1.4.10 — aucun risque d'ambiguïté, dépôts séparés) |
|
||
| **Changelogs** | TOTALEMENT SÉPARÉS : `docs/CHANGELOG.md` (web, lu par le dialog « Nouveautés ») vs `docs/CHANGELOG.md` Android (source des releases Gitea APK) — jamais fusionnés, jamais synchronisés automatiquement |
|
||
| **Asset PK** (`pk_profiles.json`) | COPIE dans chaque dépôt. ⚠️ Toute retouche (re-fit, nouvel ester) se fait dans les DEUX dépôts avec les DEUX suites de tests (pins identiques des pics ODS) — une divergence ferait échouer la CI de l'un ou l'autre |
|
||
| **Fix de moteur cross-cutting** | le même fix se fait dans les DEUX dépôts (même test épinglé, même n° de bug) — cf §11 pour l'exemple du portage |
|
||
| **Données de test réelles** (`local-test-data/`) | vivent dans le dépôt ANDROID (gitigné, jamais versionnées). Le web ne les duplique PAS : `tests/helpers.localTestDataDir()` les cherche dans le dépôt Android voisin (layout standard `~/projects/HormoneTrack*`), skip des tests si absent |
|
||
| **Commits** | convention Android conservée : commits par couche (noyau / UI / docs), release = commit dédié |
|
||
|
||
### Layout disque attendu
|
||
|
||
```
|
||
~/projects/
|
||
├── HormoneTrack/ dépôt Android (existant) — héberge local-test-data/
|
||
│ └── local-test-data/ exports réels, gitigné, JAMAIS versionnés
|
||
└── HormoneTrack-web/ dépôt web (ce projet)
|
||
```
|
||
|
||
Les tests web cherchent les exports dans le dépôt voisin : si tu clones
|
||
HormoneTrack-web ailleurs, les 22 tests de régression data-driven sont
|
||
simplement SKIPPÉS (comportement Assume identique à l'Android) — ou
|
||
dépose les exports dans un `local-test-data/` local au dépôt web (gitigné).
|
||
|
||
### Remotes du dépôt web (même double-instance que l'Android)
|
||
|
||
- `origin` → `https://gitea.cloudyfy.fr/Siphonight/HormoneTrack-web` (privé, HTTPS + trousseau)
|
||
- `farewell` → `git@farewell:Siphonight/HormoneTrack-web.git` (SSH, alias existant)
|
||
|
||
⚠️ La CRÉATION des repos sur les deux instances Gitea nécessite le scope
|
||
`write:user` (le token `write:repository` ne suffit PAS, et le
|
||
push-to-create est désactivé sur farewell) — cf §10.
|
||
|
||
## 4. Architecture & correspondance fichier par fichier
|
||
|
||
Pas de DI, pas de reactive framework : le store pub/sub notifie les écrans,
|
||
qui se re-rendent entièrement (le DOM est jetable, l'état vit dans le store
|
||
et le hash). Le routing est par **hash** (`#home`, `#chart`, `#doses`,
|
||
`#labs`, `#treatments`, `#settings`, `#treatment-edit/{id}`) — navigable
|
||
headless, lien profond possible.
|
||
|
||
```
|
||
HormoneTrack-web/ (dépôt séparé)
|
||
├── index.html page unique (SPA) — shell construit par app.js
|
||
├── css/style.css thème (couleurs = Color.kt Android), mobile-first
|
||
├── LICENSE GPL-3.0 (copie de l'Android — dépôt autonome)
|
||
├── Dockerfile image de déploiement (nginx-unprivileged, §10)
|
||
├── nginx.conf server block no-cache/gzip/sécurité (§10)
|
||
├── docker-compose.yml run durci (read_only + tmpfs, §10)
|
||
├── .dockerignore runtime seulement (docs/ INCLUS — changelog)
|
||
├── .gitignore node_modules, package-lock, local-test-data
|
||
├── assets/pk_profiles.json COPIE de l'asset Android (tables Estrannaise)
|
||
├── js/
|
||
│ ├── pk/ ── NOYAU PUR (aucun DOM/storage) — testable Node ──
|
||
│ │ ├── pk-engine.js PharmacokineticEngine.kt (1/4) : Bateman,
|
||
│ │ │ contribution d'une dose, e2At/computeCurve,
|
||
│ │ │ modèle T, convertTToNgMl, prévision
|
||
│ │ ├── pk-calibration.js engine (2/4) : échelles par ester/période/
|
||
│ │ │ modèle (#60), garde labIsSignificant (#61),
|
||
│ │ │ autoCalibrated, k T, calibration manuelle
|
||
│ │ ├── pk-reminders.js engine (3/4) : nextReminderFireFor (grille
|
||
│ │ │ Posologie, fix #52)
|
||
│ │ ├── pk-extrema.js engine (4/4) : detectExtrema (pics/creux)
|
||
│ │ ├── pk-profile-store.js PKProfileStore.kt : asset + échantillonnage,
|
||
│ │ │ lookup insensible à la casse (#22), modèle
|
||
│ │ │ strict, extrapolation terminale (#20)
|
||
│ │ ├── transfem-science-models.js V3C méta-analyse TFS (7 esters, params
|
||
│ │ │ copiés à l'identique du Kotlin)
|
||
│ │ ├── whsah-models.js fit WHSAH de Mona (6 esters, params identiques)
|
||
│ │ ├── lab-trajectory-model.js « Tracé labs » (v1.5.0 + PROLONGATION
|
||
│ │ v1.6.0) : courbe hybride + extension ρ const
|
||
│ │ M(t)×ρ(t) ancrée sur les labs (miroir strict
|
||
│ │ de pk/LabTrajectoryModel.kt — garde #61,
|
||
│ │ ρ log-linéaire, fenêtre labs)
|
||
│ ├── alerts.js Alerts.kt : seuils opt-in, codec d'état,
|
||
│ │ │ shouldNotify (anti-spam)
|
||
│ │ ├── presets.js PKPresets.kt (25 presets — v1.9.4 :
|
||
│ │ │ les 6 esters injectables ESE)
|
||
│ │ ├── chart-helpers.kt fonctions pures de CurveChart.kt :
|
||
│ │ │ panDeltaHours (#62), xLabelTicks (#55),
|
||
│ │ │ pointHoursBefore (#53), clampPanHours (#54),
|
||
│ │ │ niceCeil, stepForRange, forecast*,
|
||
│ │ │ defaultModelToggles (#58) + helpers Intl
|
||
│ │ └── index.js barrel (point d'import des écrans/tests)
|
||
│ ├── data/
|
||
│ │ ├── models.js Treatment/DoseLog/LabResult (CHAMPS GSON
|
||
│ │ │ IDENTIQUES — ne jamais renommer, cf §6),
|
||
│ │ │ enums, helpers (choicesForModel, labels)
|
||
│ │ ├── store.js persistance localStorage + pub/sub + import
|
||
│ │ │ écrasement (miroir Room + BackupManager)
|
||
│ │ └── backup.js BackupData v2 : build/parse compat Android,
|
||
│ │ noms de fichiers d'export (ExportFileNames)
|
||
│ ├── util/
|
||
│ │ ├── i18n.js dictionnaires FR/EN + t() (+ presets)
|
||
│ │ ├── format.js formatDose/formatLabValue/formatValue,
|
||
│ │ │ HrtDuration, formatage Intl des dates
|
||
│ │ ├── changelog.js ChangelogHelper.kt (isVersionNewer,
|
||
│ │ │ sectionsSince — comparaison numérique #38)
|
||
│ │ └── app-log.js AppLog.kt : journal 500 lignes, fail-safe
|
||
│ └── ui/ ── ÉCRANS (DOM uniquement ici) ──
|
||
│ ├── app.js HormoneTrackApp+MainActivity+Root : shell,
|
||
│ │ routing hash, « Nouveautés » (markdown
|
||
│ │ rendu), hook ?demo=1, boucle ticks
|
||
│ ├── components.js el(), showDialog/toast, fields, switch,
|
||
│ │ chips, FAB, dateTimeField (M3 → natifs)
|
||
│ ├── dialogs.js DoseDialog.kt (override ester par modèle)
|
||
│ │ + LabDialog.kt (E2+T une entrée)
|
||
│ ├── home.js HomeScreen.kt (niveau actuel #53, alertes,
|
||
│ │ prochaine dose, log rapide, mini-chart)
|
||
│ ├── chart.js ChartScreen.kt : fenêtre/pan/zoom (fixes
|
||
│ │ #54/#56/#62), toggles, calibration par
|
||
│ │ modèle (#60), prévision
|
||
│ ├── chart-canvas.js CurveChart.kt : rendu Canvas multi-séries,
|
||
│ │ labs orange, marqueurs, extrema
|
||
│ ├── doses.js DosesScreen.kt (groupes jour, Δ jours,
|
||
│ │ temps sous THS — 3 placeholders, bug #47)
|
||
│ ├── labs.js LabsScreen.kt (groupement, suppression
|
||
│ │ par prise entière, sélecteur E2/T)
|
||
│ ├── treatments.js TreatmentsScreen.kt (cartes, modelLabelKey #59)
|
||
│ ├── treatment-editor.js TreatmentEditorScreen.kt (presets,
|
||
│ │ calibration, Posologie, rappel, actif)
|
||
│ ├── settings.js SettingsScreen.kt (langue, auto-cal, T,
|
||
│ │ seuils, fuseau, notifs, backup, logs, à propos)
|
||
│ └── reminders.js ReminderManager+AlertNotifier (web) :
|
||
│ boucle 30 s rappels + 5 min seuils,
|
||
│ Notification API + bannière in-app
|
||
├── docs/
|
||
│ ├── DEVELOPPEMENT.md cette doc
|
||
│ └── CHANGELOG.md journal web (lu par le dialog « Nouveautés »)
|
||
├── scripts/
|
||
│ ├── serve.py serveur statique de dev (no-cache, MIME ok)
|
||
│ ├── check.sh vérifications 1→6 (syntaxe, i18n, tests, E2E,
|
||
│ │ smoke HTTP) + mode --release : 7→9 (cohérence
|
||
│ │ version↔changelog↔tag, arbre propre, test
|
||
│ │ conteneur si daemon — §10)
|
||
│ ├── i18n-check.mjs cohérence FR/EN + clés utilisées
|
||
│ ├── e2e.mjs E2E playwright (Firefox headless, ?demo=1 ;
|
||
│ │ mode conteneur via HRT_E2E_BASE)
|
||
│ ├── container-test.sh build + run DURCI + headers/gzip/MIME +
|
||
│ │ E2E contre le conteneur ([port] [platform])
|
||
│ └── publish-release.py publie la release Gitea (zip = git archive
|
||
│ AU TAG, vérifié par téléchargement)
|
||
├── tests/ ── node --test (miroirs des suites Kotlin) ──
|
||
│ ├── helpers.js fixtures (makeTreatment/makeDose/makeLab, asset)
|
||
│ ├── pk-profiles.test.js ← PKProfileStoreTest.kt (pics ODS, casse #22…)
|
||
│ ├── tfs-models.test.js ← TransfemScienceModelsTest.kt (article ±2 %)
|
||
│ ├── whsah-models.test.js ← WhsahModelsTest.kt (Mona ±2 %)
|
||
│ ├── pk-engine.test.js ← PharmacokineticEngineTest.kt (+ #35, §6.bis)
|
||
│ ├── calibration.test.js ← CalibrationPerModel/ScaleFactorWhsahRepro/
|
||
│ │ V120Features (fixes #60/#61, périodes d'ester)
|
||
│ ├── estrannaise-models.test.js ← EstrannaiseModelsTest.kt (v1.9.0 :
|
||
│ │ fidélité RMS 0, pics, dégénérés, MCMC
|
||
│ │ 313×6, t½ analytique)
|
||
│ ├── estrannaise-cloud.test.js ← EstrannaiseCloudTest.kt (v1.9.0 :
|
||
│ │ 32 courbes, dispersion, exclusivité)
|
||
│ ├── rounding.test.js ← round2/round3 arrondis (v1.9.2 :
|
||
│ │ harmonisation Android, fin de la
|
||
│ │ troncature historique)
|
||
│ ├── presets.test.js ← PKPresetsTest.kt (v1.9.4 : presets des
|
||
│ │ 6 esters ESE, unicité, i18n EN/FR,
|
||
│ │ choicesForModel aligné dispatchs)
|
||
│ ├── lab-timing.test.js ← LabTimingTest.kt (v1.8.0 : prochaine
|
||
│ │ prise de sang recommandée — creux avant
|
||
│ │ créneau, saut de stabilisation, gardes)
|
||
│ ├── lab-notes.test.js ← labNotesForDisplay (v1.7.1 : notes d'une
|
||
│ │ prise E2+T — les DEUX distinctes affichées)
|
||
│ ├── lab-trajectory-model.test.js ← LabTrajectoryModelTest.kt (v1.5.0
|
||
│ │ « Tracé labs » + v1.6.0 prolongation —
|
||
│ │ passage exact, garde #61, horizon cutoff)
|
||
│ ├── backup.test.js ← BackupGsonTest.kt + exports Android RÉELS
|
||
│ │ (local-test-data/ si présent, sinon skip)
|
||
│ ├── chart-helpers.test.js ← ChartZoomTest.kt (#53/#54/#55/#58/#62)
|
||
│ ├── alerts.test.js ← AlertsTest.kt (codec, anti-spam)
|
||
│ ├── misc.test.js ← ReminderSchedule/Extrema/Changelog/HrtDuration
|
||
│ └── store.test.js CRUD, IDs auto, CASCADE, pub/sub, corruption
|
||
└── package.json scripts npm + devDependencies (E2E seulement)
|
||
```
|
||
|
||
### Points clés de l'architecture
|
||
|
||
- **Le noyau `js/pk/` est PUR** : aucune importation de `data/`, `ui/` ou
|
||
DOM → testé en Node exactement comme le Kotlin l'est en JVM. C'est la
|
||
garantie que les courbes web == courbes Android.
|
||
- **Le store est le seul point de persistance** : les écrans ne touchent
|
||
jamais localStorage directement ; les mutations passent par
|
||
`store.*` qui notifie les abonnés.
|
||
- **`isActive` = drapeau administratif** (Android §6.bis, appliqué tel quel) :
|
||
la saisie (chips, dropdown création) filtre les actifs ; la simulation,
|
||
la calibration et les rappels du moteur reçoivent TOUS les traitements.
|
||
- **Calibration par modèle affiché** (fix #60) : l'écran Graphiques calcule
|
||
une `autoCalibrated` par modèle (ESE/TFS/WHS) ; l'accueil utilise le
|
||
modèle stocké — comme l'Android.
|
||
- **« 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).
|
||
|
||
## 5. Persistance (localStorage) & schéma de données
|
||
|
||
Clés (préfixe `hormonetrack.`) — miroir des tables Room / clés DataStore :
|
||
|
||
| Clé | Contenu | Équivalent Android |
|
||
|---|---|---|
|
||
| `treatments` | tableau JSON d'objets Treatment (champs = entités Room) | table `treatments` |
|
||
| `doseLogs` | idem DoseLog (FK `treatmentId`, CASCADE au delete) | table `dose_logs` |
|
||
| `labResults` | idem LabResult | table `lab_results` |
|
||
| `tConfig` | `{base, floor, k}` (défauts 6.0 / 0.2 / 0.19) | DataStore `t_base/t_floor/t_k` |
|
||
| `settings` | `{language, autoCalibrate, alertE2High/Low, alertTHigh/Low, chartTimezone, changelogSeenVersion, alertNotifiedState}` (fusion avec défauts à la lecture) | DataStore (plusieurs clés) |
|
||
| `debugLog` | texte, `\n` (buffer 500 lignes) | `filesDir/debug-log.txt` |
|
||
|
||
Règles :
|
||
|
||
- **IDs auto-incrémentés par table** (max + 1) — miroir Room
|
||
`autoGenerate` ; à l'import, les IDs du backup sont conservés (FK
|
||
dose→traitement) et les compteurs repartent de max+1.
|
||
- **Import = écrasement** (miroir v1.2.6) : effacement enfants→parents puis
|
||
restauration, un seul `emit()` final.
|
||
- **Donnée corrompue ≠ crash** : `read()` retombe sur le défaut (même
|
||
philosophie qu'AppLog.init Android).
|
||
- `calendarEventId` existe dans les objets (fidélité du schéma) mais reste
|
||
TOUJOURS `null` côté web.
|
||
|
||
## 6. Compatibilité des backups avec l'Android
|
||
|
||
Format `BackupData` v2 — **verrouillé par des tests des deux côtés** :
|
||
|
||
```json
|
||
{
|
||
"version": 2,
|
||
"exportedAt": 1749000000000,
|
||
"treatments": [ { "id": 1, "name": "…", "type": "ESTRADIOL", "route": "INJECTION_SUBCUT",
|
||
"doseAmount": 5, "doseUnit": "mg", "isActive": true, "notes": null,
|
||
"esterType": "EEN", "pkModel": "TFS",
|
||
"absorptionHours": 156, "eliminationHalfLifeHours": 110,
|
||
"bioavailabilityFraction": 1, "scaleFactor": 1,
|
||
"forecastIntervalDays": 7, "reminderHour": 18, "reminderMinute": 0,
|
||
"reminderEnabled": true, "calendarEventId": null, "createdAt": … } ],
|
||
"doseLogs": [ { "id": 1, "treatmentId": 1, "timestamp": …, "doseAmount": 5,
|
||
"notes": null, "esterType": null } ],
|
||
"labResults": [ { "id": 1, "marker": "E2", "value": 300, "unit": "pg/mL",
|
||
"timestamp": …, "notes": null } ],
|
||
"tConfig": { "base": 6.0, "floor": 0.2, "k": 0.19 },
|
||
"settings": { "language": "fr", "autoCalibrate": false,
|
||
"alertE2High": null, "alertE2Low": null,
|
||
"alertTHigh": null, "alertTLow": null }
|
||
}
|
||
```
|
||
|
||
- **Champs PLATS** dans `settings` (miroir `UserSettings.kt` — Gson lit par
|
||
réflexion côté Android). ⚠️ Les clés web extra (`chartTimezone`,
|
||
`alertNotifiedState`, `changelogSeenVersion`) sont **exclues** de
|
||
l'export (testé) — un backup importé dans l'Android ne doit rien contenir
|
||
d'inconnu.
|
||
- **Rétrocompat v1** : backup sans `settings` → `settings = null` →
|
||
l'import ne touche pas aux réglages (comportement Gson identique).
|
||
- **L'import web est un écrasement** (dialog d'avertissement, bouton
|
||
« Effacer & restaurer ») ; le `tConfig` et les réglages du backup sont
|
||
restaurés comme sur l'Android.
|
||
- `tests/backup.test.js` importe les **vrais exports Android** via
|
||
`helpers.localTestDataDir()` (dépôt Android voisin, skip si absent —
|
||
comportement `Assume` identique au Kotlin) — garde de confidentialité
|
||
identique : ces fichiers restent HORS dépôts, et les assertions sont
|
||
data-driven (aucune valeur réelle en dur).
|
||
|
||
**Règle de maintenance** : tout changement de schéma (côté Android OU web)
|
||
doit (a) passer par les deux suites de tests backup, (b) rester
|
||
rétrocompatible, (c) être documenté dans les DEUX docs de dev.
|
||
|
||
## 7. Fuseaux horaires (Intl)
|
||
|
||
L'Android utilise `Calendar`/`TimeZone` ; le web utilise `Intl` :
|
||
|
||
- `zonedParts(ms, tz)` = `Calendar.getInstance(tz).get(...)` (via
|
||
`Intl.DateTimeFormat.formatToParts`) ;
|
||
- `zonedTimeToMs(parts, tz)` = construction inverse avec **deux passes de
|
||
correction DST** (mesure de l'offset à l'instant naïf, puis à l'instant
|
||
corrigé — point fixe) ;
|
||
- `xLabelTicks` (fix #55) : les pas ≥ 24 h sont alignés sur **minuit LOCAL
|
||
du fuseau de lecture** (Paramètres → « Fuseau du graphique », vide =
|
||
navigateur), avancés de N jours CALENDAIRES (DST-safe, testé sur la
|
||
traversée de fin mars) ; les pas horaires sur des heures rondes locales.
|
||
|
||
Pièges rencontrés :
|
||
|
||
- `Intl` renvoie `hour: '24'` avec `h24` et pas `h23` — toujours
|
||
`hourCycle: 'h23'` ;
|
||
- un formatter avec SEULEMENT `minute` ne pads pas à 2 chiffres partout —
|
||
tester sur `(hour, minute)` ensemble ;
|
||
- `-0` : `Math.trunc(-0.8) === -0` — `assert.equal(-0, 0)` échoue en strict ;
|
||
les helpers normalisent (`clampPanHours`, `panDeltaHours`).
|
||
|
||
## 8. Tests — processus complet
|
||
|
||
### Panorama
|
||
|
||
| Niveau | Outil | Couverture | Commande |
|
||
|---|---|---|---|
|
||
| Syntaxe | `node --check` | tous les modules ES | `bash scripts/check.sh` |
|
||
| i18n | `scripts/i18n-check.mjs` | clés FR/EN synchronisées + clés utilisées | (dans check.sh) |
|
||
| Unitaires | `node --test` | **132 tests** : noyau PK, calibration, backup, store, helpers, alertes, rappels, changelog, tracé labs (v1.5.0) | `npm test` |
|
||
| E2E navigateur | playwright-core + build Firefox | app réelle : rendu, canvas peint (pixels), navigation, dialog changelog, langue | `npm run e2e` |
|
||
| Smoke HTTP | curl | ressources clés en 200 | `check.sh --with-serve` |
|
||
|
||
La commande unique avant tout push : **`bash scripts/check.sh`** (tout
|
||
exécuter, E2E inclus s'il est installé).
|
||
|
||
### Installation (une fois)
|
||
|
||
```bash
|
||
cd web
|
||
npm i # devDependencies (playwright) — l'APP n'en a aucune
|
||
npx playwright install firefox # le build Firefox piloté (~90 Mo, cache ~/.cache)
|
||
```
|
||
|
||
⚠️ Le **Firefox système** ne convient PAS : depuis ~Firefox 150, le
|
||
protocole Juggler de Playwright n'y est plus fonctionnel (exit immédiat au
|
||
launch) — d'où le build dédié dans le cache Playwright, hors dépôt.
|
||
|
||
### Ce que chaque suite épinglera TOUJOURS
|
||
|
||
- **pk-profiles** : les 6 profils × 8001 points, pics == valeurs ODS
|
||
(61,12 / 3,44 / 31,35 / 58,96 / 10,11 / 31,97), casse `EEn` (#22),
|
||
modèle strict (#21), extrapolation ≥ 1 % du pic (#20).
|
||
- **tfs-models** : pics/t½ des Tableaux 9–10 ±2 %, équilibre EV 5 mg/7 j =
|
||
Figure 11, PEP, EU ultra-rapide. *Toute retouche de
|
||
transfem-science-models.js passe par là.*
|
||
- **whsah-models** : fidélité Mona ±2 %, PEP non couvert, EEn J+1 ×3.
|
||
- **pk-engine** : pic Bateman ≈ Tmax (bisection #19), dispatch 3 modèles,
|
||
override par dose, §6.bis (inactif simulé), prévision (#35), T monotone.
|
||
- **calibration** : par période d'ester, **par modèle** (#60, chaque courbe
|
||
calibrée passe par le lab), garde #61 (labs hors fenêtre exclus des DEUX
|
||
pipelines — jamais de médiane contaminée), k T contre l'E2 CALIBRÉE.
|
||
- **lab-trajectory-model** : v1.5.0 « Tracé labs » — miroir du Kotlin
|
||
(passage exact, ρ log-linéaire, garde #61, fenêtres, indépendance du
|
||
scaleFactor, modelOverride, doublons, bornes). v1.6.0 **prolongation**
|
||
(miroir §7.10.bis Android) : identité `M × ρ_last` exacte au-delà du
|
||
dernier lab, suture exacte, horizon = cutoff du traitement
|
||
(`extensionHorizonEndMs` — gardes : pas de dose / modèle déjà éteint →
|
||
null), dose EV loguée après le dernier lab → pic de prolongation,
|
||
garde #61 sur la prolongation, flag off = v1.5.0 bit-compatible,
|
||
`lastAnchorMs`.
|
||
- **lab-timing** : v1.8.0 « prochaine prise de sang recommandée » (miroir
|
||
du Kotlin, §7.11 Android) : creux = minimum de la courbe prévisionnelle
|
||
entre 2 injections, saut au premier creux STABILISÉ (5 × t½ — TFS/WHS
|
||
analytiques, Estrannaise lue dans la table via terminalHalfLifeDays),
|
||
filtres (≥ now+6 h, > dernière prise, Posologie requise), invite
|
||
« renseigne une Posologie », oral seul → null.
|
||
- **estrannaise-models / estrannaise-cloud / rounding** : v1.9.0/v1.9.2 —
|
||
modèle Estrannaise ANALYTIQUE (fidélité RMS 0 vs tables ODS, pics ±0,2 %,
|
||
cas dégénérés sans NaN, MCMC 313×6 esters, t½ analytique EV=ln2/0,236)
|
||
+ nuage d'incertitude (32 courbes à dispersion réelle, fenêtre,
|
||
exclusivité ESE — TFS/oral hors nuage, gardes vides) + **round2/round3
|
||
ARRONDIS** (v1.9.2 : fin de la troncature historique — harmonisation des
|
||
niveaux web/Android, cf CHANGELOG Android 1.8.2) ;
|
||
- **presets** : v1.9.4 (miroir `PKPresetsTest.kt`) — les presets ESE
|
||
couvrent les 6 esters du fit analytique (unicité, clés i18n EN/FR),
|
||
TFS (7) / WHSAH (6) inchangés, et **`choicesForModel` aligné sur les
|
||
dispatchs** (ESE 6 / TFS 7 explicite sans EUCS / WHS 6) — l'éditeur et
|
||
le dialog de dose ne peuvent proposer un ester que le moteur sait
|
||
tracer ;
|
||
- **backup** : round-trip v2, rétrocompat v1, écrasement + IDs conservés,
|
||
**imports des exports Android réels** (data-driven, hors dépôt).
|
||
- **chart-helpers** : cumul fractionnaire du pan (#62), minuit local (#55),
|
||
pointHoursBefore (#53), clamps (#54), toggles (#58).
|
||
- **alerts / misc / store** : codec strict, anti-spam, grille Posologie
|
||
(#52), extrema, comparaison numérique de versions (#38), résilience du
|
||
store (donnée corrompue → défauts).
|
||
|
||
### Écrire un nouveau test
|
||
|
||
1. Le code visé doit être dans `js/pk/` (pur) ou testable via le store
|
||
(backend Map) — l'UI est couverte par l'E2E.
|
||
2. Réutiliser `tests/helpers.js` (`makeTreatment/makeDose/makeLab`,
|
||
`initProfiles`, `assertClose`).
|
||
3. ⚠️ Les fixtures ont des **defaults identiques aux entités Kotlin** :
|
||
`id: 0` = « pas en base » (un `id: 1` par défaut a faussé trois tests
|
||
pendant le portage — cf §11).
|
||
4. Les scénarios de labs/calibration se posent **data-driven** :
|
||
`lab = e2At(...) × facteur` — ne jamais recalculer la prédiction à la
|
||
main (l'accumulation est celle du moteur).
|
||
|
||
### E2E (scripts/e2e.mjs)
|
||
|
||
- Démarre `serve.py` sur un port libre en argument, lance le Firefox
|
||
Playwright headless, navigue avec `?demo=1` (données fictives banniérées,
|
||
jamais sur un store non vide) ;
|
||
- vérifie le **rendu réel** : cartes, valeurs, dialog « Nouveautés »
|
||
(markdown rendu), navigation des 5 onglets + Paramètres, **pixels du
|
||
canvas** (`getImageData` — le graphique PEINT vraiment) ;
|
||
- **v1.6.0 : scénario « Tracé labs prolongé »** — dézoome (dernier lab de
|
||
la démo à now − 7 j), vérifie que `Prolonger` est DÉSACTIVÉ tant que
|
||
`Tracé labs` est off, active les deux chips, asserting la légende
|
||
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é).
|
||
- **v1.8.0 : encart « prochaine prise de sang »** sur #labs (démo EEn 7 j)
|
||
— titre, creux daté, créneau d'injection associé, mention de stabilisation ;
|
||
- 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"]`
|
||
(posé par `renderRoute` une fois l'asset chargé) — attendre ce sélecteur,
|
||
jamais un `sleep` nu.
|
||
- ⚠️ Le seed de langue (`addInitScript`) est **conditionnel** : l'écraser à
|
||
chaque navigation effacerait `changelogSeenVersion` et ferait réapparaître
|
||
le dialog à chaque `page.goto` (une goto = reboot complet de la SPA).
|
||
|
||
## 9. Workflow de développement
|
||
|
||
```bash
|
||
cd web
|
||
python3 scripts/serve.py # → http://127.0.0.1:8970 (no-cache : F5 = code à jour)
|
||
# … développement …
|
||
bash scripts/check.sh # AVANT chaque commit (syntaxe + i18n + tests + E2E)
|
||
```
|
||
|
||
Règles de code (appliquées partout, à conserver) :
|
||
|
||
- **Tout fichier commence par un bloc KDoc** qui dit QUOI il porte (fichier
|
||
Kotlin d'origine), POURQUOI il existe et les invariants à préserver.
|
||
- **Toute fonction non triviale a un JSDoc** avec `@param`/`@returns` et,
|
||
quand c'est un fix Android transposé, le numéro du bug (#NN).
|
||
- **Le noyau `js/pk/` ne connaît ni le DOM ni le storage** — si une feature
|
||
exige l'inverse, c'est le design qu'il faut revoir.
|
||
- **Strings** : passer par `t('clé')`, dans les DEUX dictionnaires
|
||
(`i18n-check.mjs` bloque sinon) — leçon Android §12.
|
||
- **Champs de modèles** : ne JAMAIS renommer un champ de
|
||
`data/models.js` sans vérifier `BackupManager.kt` (compat Gson, cf §6).
|
||
- **Tout fix de bug** : test qui l'épingle d'abord (rouge), puis fix, puis
|
||
§11 de cette doc + `docs/CHANGELOG.md`.
|
||
|
||
## 10. Processus de push & releases (sync Android)
|
||
|
||
Les releases web sont **SYNCHRONISÉES avec les releases APK** (décision du
|
||
8 sept. 2026) : chaque feature release de l'Android est portée et tagguée
|
||
au MÊME numéro dans ce dépôt — l'utilisatrice a toujours la parité des
|
||
fonctionnalités (sauf impossibilités structurelles §12).
|
||
|
||
**v1.4.10 = la première release web publiée (8 sept. 2026, cloudyfy)** :
|
||
tag + release Gitea avec l'archive zip de déploiement, APRES validation
|
||
complète (132 tests unitaires + E2E navigateur local + container-test réel
|
||
en configuration durcie). Le dépôt farewell reste en attente de la création
|
||
du repo (one-shot côté Gitea).
|
||
|
||
### Remotes
|
||
|
||
```
|
||
origin https://gitea.cloudyfy.fr/Siphonight/HormoneTrack-web (privé, HTTPS + trousseau)
|
||
farewell git@farewell:Siphonight/HormoneTrack-web.git (SSH, alias existant)
|
||
```
|
||
|
||
⚠️ **Création des repos** (one-shot, faite par l'utilisatrice — le token
|
||
`write:repository` ne permet PAS de créer un repo via API, et le
|
||
push-to-create est désactivé sur farewell) : créer `HormoneTrack-web` sur
|
||
les DEUX instances (cloudyfy + farewell), privé, puis configurer les
|
||
remotes ci-dessus. (Historique : Android a fait la même opération au
|
||
push initial v1.2.3, cf doc Android §16.)
|
||
|
||
### Checklist de release SYNC (Android + web, une seule session)
|
||
|
||
1. **Android** (doc Android §16 checklist complète) : bump `versionName`/
|
||
`versionCode`, tests + lint verts, section `docs/CHANGELOG.md`, commit,
|
||
tag annoté `vX.Y.Z`.
|
||
2. **Web** (cette doc) :
|
||
a. porter la/les features si pas déjà fait (commits par couche) ;
|
||
b. `WEB_VERSION = "X.Y.Z"` dans `js/ui/settings.js` **= même numéro que
|
||
l'Android** + section `## [X.Y.Z]` en tête de `docs/CHANGELOG.md`
|
||
(le dialog « Nouveautés » la lira) ;
|
||
c. `bash scripts/check.sh` vert (E2E inclus) ;
|
||
d. commit `vX.Y.Z — résumé du portage` + tag annoté `vX.Y.Z`.
|
||
3. **Push des deux dépôts** :
|
||
```bash
|
||
# Android
|
||
git push origin main --tags && git push farewell main --tags
|
||
# Web
|
||
git push origin main && git push origin vX.Y.Z
|
||
git push farewell main && git push farewell vX.Y.Z
|
||
```
|
||
⚠️ Ne JAMAIS comparer les versions en chaînes (piège #38 Android) —
|
||
tout script futur passe par une comparaison numérique.
|
||
4. **Releases Gitea** :
|
||
- Android : `publish-release.py` sur les deux instances (2 APK), cf doc
|
||
Android §16.bis ;
|
||
- Web : `python3 scripts/publish-release.py cloudyfy vX.Y.Z` —
|
||
crée/met à jour la release (corps = section `docs/CHANGELOG.md`) et
|
||
attache l'archive `HormoneTrack-web-vX.Y.Z.zip` construite PAR
|
||
`git archive` AU TAG (jamais le working tree), vérifiée par
|
||
téléchargement (uid des leçons #37/#43 Android : une invocation fait
|
||
tout, vérif par téléchargement, échec bruyant). Idem `farewell` une
|
||
fois le repo créé.
|
||
5. **Smoke-test** : Android sur le Pixel 9 (checklist §21 Android) ; web
|
||
sur le navigateur : import d'un backup réel, pan/zoom, export/import,
|
||
langue.
|
||
|
||
### Avant CHAQUE push (release ou non)
|
||
|
||
1. `bash scripts/check.sh` **vert** (E2E inclus si installé) ;
|
||
2. les données de test réelles restent **hors des DEUX dépôts**
|
||
(`git check-ignore -v local-test-data/…` côté Android ; le web ne
|
||
doit contenir AUCUN export réel — `git log --all -- local-test-data`
|
||
vide si le dossier local existe) ;
|
||
3. si l'asset PK change : les DEUX dépôts dans la même session (tests
|
||
pins identiques) — jamais un seul côté.
|
||
|
||
### Déploiement — Docker (recommandé pour self-host)
|
||
|
||
L'app étant un site **100 % statique**, la conteneurisation est triviale :
|
||
l'image ajoute UNIQUEMENT un serveur web. Points structurants :
|
||
|
||
| Aspect | Décision | Pourquoi |
|
||
|---|---|---|
|
||
| Image de base | `nginxinc/nginx-unprivileged:alpine` | run en uid 101 (pas de root), port 8080, writes limités à /tmp — meilleure pratique conteneur, ~50 Mo |
|
||
| Données | **AUCUNE dans l'image** (`.dockerignore` exclut aussi `local-test-data` en filet de sécurité) | le conteneur distribue des fichiers ; la vie privée est identique (localStorage clients) — cf §3/§5 |
|
||
| Contenu servi | index.html + css/js/assets + **docs/** (⚠️ le dialog « Nouveautés » fetch `docs/CHANGELOG.md` au démarrage) | pas de `tests/`, `scripts/`, npm dans l'image (audit facile : `find /usr/share/nginx/html`) |
|
||
| Cache | `no-cache, must-revalidate` sur TOUT | l'app = un jeu de fichiers qui doivent rester COHÉRENTS entre eux ; un JS périmé au moment d'une mise à jour = état incohérent. Total ≈ 600 Ko + gzip sur l'asset PK (550 Ko → ~150 Ko) : coût nul |
|
||
| Ports | 8080 (non privilégié) | l'image unprivileged ne peut pas binde <1024 |
|
||
| Durcissement | `read_only: true` + tmpfs `/var/cache/nginx` + `/run`, `no-new-privileges` | site statique : AUCUNE écriture nécessaire ; si un hôte compose refuse les tmpfs, retirer les deux lignes |
|
||
| Healthcheck | busybox `wget` sur `/` (30 s/3 s) | intégré au Dockerfile — orchestrateurs (compose/NAS) savent si le site répond |
|
||
|
||
**Fichiers** : `Dockerfile`, `nginx.conf` (server block remplacé),
|
||
`.dockerignore` (exclusions documentées — `docs/` reste inclus),
|
||
`docker-compose.yml` (durcissement inclus).
|
||
|
||
```bash
|
||
# ── Build + run ──
|
||
docker compose up -d # build + run → http://<hôte>:8080/
|
||
docker compose up -d --build # rebuild après git pull (mise à jour = 30 s)
|
||
docker tag hormonetrack-web:latest hormonetrack-web:vX.Y.Z # garder un historique d'images par release sync
|
||
```
|
||
|
||
**Valider le conteneur avec le MÊME test E2E que le CI** (le script pilote
|
||
n'importe quel serveur via `HRT_E2E_BASE` — à lancer sur l'hôte qui a
|
||
Docker ET exécute `npm i && npx playwright install firefox` une fois, cf
|
||
§8) :
|
||
|
||
```bash
|
||
docker compose up -d --build
|
||
HRT_E2E_BASE=http://127.0.0.1:8080 node scripts/e2e.mjs
|
||
# → les mêmes ~30 assertions (rendu, canvas peint pixel par pixel,
|
||
# navigation, écran changelog, 0 erreur console) contre le site SERVI
|
||
# PAR LE CONTENEUR — un bug nginx/MIME casse l'app, le test le voit.
|
||
```
|
||
|
||
**Fichiers servis vérifiés** : `/`, `/css/style.css`, `/assets/pk_profiles.json`,
|
||
`/js/ui/app.js`, `/docs/CHANGELOG.md` — les mêmes que le smoke HTTP de
|
||
`check.sh --with-serve`.
|
||
|
||
**Statut (8 sept. 2026)** : build et run RÉELS validés localement via
|
||
colima (daemon Docker léger, 2 CPU / 2 Go) — `scripts/container-test.sh`
|
||
vert en configuration DURCIE pour **les DEUX architectures** (arm64 native
|
||
et linux/amd64 cross-compilée : healthcheck healthy, endpoints, headers,
|
||
gzip, MIME, E2E complet contre le conteneur). Problèmes réels trouvés aux
|
||
premiers tests, corrigés et documentés : (a) tmpfs `/tmp` REQUIS en
|
||
read-only (« mkdir /tmp/proxy_temp failed » sinon), (b) curl `%header_json`
|
||
minuscule les clés (bug du TEST), (c) **exec format error sur l'hôte
|
||
amd64** (cf §10 bis). Si le durcissement pose problème sur ton NAS :
|
||
retirer `read_only`/`tmpfs`/`security_opt` du compose (aucune perte de
|
||
sécurité cruciale — le conteneur ne détient rien).
|
||
|
||
### Cross-arch (ⓘ leçon du 8 sept. 2026 — « exec /docker-entrypoint.sh:
|
||
exec format error »)
|
||
|
||
`exec format error` au `docker run` = l'image est pour une AUTRE
|
||
architecture CPU que l'hôte (image arm64 buildée sur Mac → serveur amd64).
|
||
Le Dockerfile ne contient **AUCUN `RUN`** (labels + COPY + HEALTHCHECK) :
|
||
la cross-compilation est FAIBLE — buildx produit l'image pour n'importe
|
||
quelle platform SANS émulation de build (constaté : build amd64 instantané
|
||
sur l'arm64). Seul le RUN du conteneur émule (Rosetta via colima VZ / qemu
|
||
sur l'hôte de build — sous émulation nginx journalise un
|
||
`io_setup() failed (ENOSYS)` inoffensif : il retombe et répond 200 ; ce
|
||
syscall existe sur un vrai Linux amd64).
|
||
|
||
**Règle** : le conteneur de PROD doit être buildé pour L'ARCH de l'hôte —
|
||
deux procédures équivalentes :
|
||
|
||
1. **Sur le serveur cible** (recommandé — zéro registre, zéro émulation) :
|
||
```bash
|
||
git clone https://gitea.cloudyfy.fr/Siphonight/HormoneTrack-web && cd HormoneTrack-web
|
||
docker compose up -d --build # arch native du serveur, toujours bon
|
||
```
|
||
2. **Multi-arch depuis le Mac** (push vers un registry — Docker Hub, GHCR,
|
||
ou le registry interne du NAS) :
|
||
```bash
|
||
docker buildx build --platform linux/amd64,linux/arm64 -t <user>/hormonetrack-web:latest --push .
|
||
```
|
||
(build natif des deux archs — cf container-test.sh [platform] pour
|
||
tester CHAQUE arch localement avant push : l'E2E complet tourne contre
|
||
l'image cross-archémulée.)
|
||
|
||
**Test de l'arch cible AVANT push/déploiement** :
|
||
`bash scripts/container-test.sh <port> linux/amd64` — c'est le test qui
|
||
aurait attrapé l'incident « exec format error » du premier déploiement
|
||
NAS (image arm64 seule déployée sur hôte amd64).
|
||
|
||
Composition de l'utilisatrice (8 sept.) : service `image:` depuis un
|
||
registry, `env_file`/limites de ressources/réseaux externes — tout est
|
||
inoffensif pour ce conteneur statique (aucune variable lue, no volumes).
|
||
|
||
## 11. Bugs potentiels évités pendant le portage
|
||
|
||
Trouvés pendant l'écriture (à ne pas réintroduire) :
|
||
|
||
1. **Propriétés dérivées Kotlin = fonctions JS** : `usesProfileModel` est un
|
||
*getter* en Kotlin mais n'existe pas sur les objets JSON plats → le
|
||
moteur retombait silencieusement en Bateman (courbes ÷100). Fix :
|
||
`usesProfileModel(treatment)` dans `pk-engine.js`. Toute propriété
|
||
dérivée d'entité (`isInjection`, …) doit être recalculée, jamais lue.
|
||
2. **Fixtures de test** : `makeTreatment` avec `id: 1` par défaut (au lieu
|
||
de 0 = « pas en base ») faisait échouer auto-increment/CASCADE. Defaults
|
||
des helpers == valeurs d'insertion Kotlin.
|
||
3. **Extrapolation terminale** : première version linéaire (pente) au lieu
|
||
du **taux logarithmique** du Kotlin — queues croissantes sur EU (plancher
|
||
0,01/0,00). Portage fidèle exigé, tests EU/EV des deux régimes.
|
||
4. **`zonedTimeToMs`** : la 2ᵉ passe de correction DST doit mesurer
|
||
l'offset à l'instant CORRIGÉ (`naive − offset(instant)`) — une première
|
||
version référençait le mauvais instant (ticks à minuit UTC+2 = 02:00).
|
||
5. **`-0`** : `Math.trunc`/négation produisent `-0` ; `assert.equal(-0, 0)`
|
||
échoue en strict — normaliser dans les helpers (`panDeltaHours`,
|
||
`clampPanHours`).
|
||
6. **Imports relatifs** : `app.js` vit dans `js/ui/` — un import écrit comme
|
||
depuis `js/` donne des 404 MIME silencieuses (le module root ne charge
|
||
PAS, aucune erreur fatale visible). D'où le check `node --check` +
|
||
l'E2E « aucune erreur console ».
|
||
7. **Exports du barrel** : `pk/index.js` doit ré-exporter tout ce que les
|
||
écrans importent nommément (`pointHoursBefore`, …) — une omission =
|
||
SyntaxError au chargement. L'E2E l'attrape (page vide).
|
||
8. **Assignation à une variable non déclarée** en ESM (strict) = crash du
|
||
module entier (`lastRoute` déclaré dans `main()` mais assigné dans
|
||
`renderRoute()`) → remonter l'état partagé au niveau module.
|
||
9. **Closures de dialog** : `const dlg = showDialog({ actions: [… dlg.close()] })`
|
||
— oublier le `const` donne une ReferenceError AU CLIC (pas au chargement),
|
||
invisible sans test d'interaction. L'E2E clique maintenant les actions.
|
||
10. **Seed de test conditionnel** : un `addInitScript` non conditionnel
|
||
écrase `settings` à chaque navigation (re-lecture du changelog à chaque
|
||
goto). Cf §8.
|
||
11. **`getSettings()` fusionne les défauts** : un stockage partiel ne doit
|
||
jamais exposer des champs `undefined` (les gardes `!== null` deviennent
|
||
fausses autrement).
|
||
12. **Miroir du fix #63 Android** (v1.5.0, étendu v1.6.0) : les clés
|
||
`'LAB'` ET `'LABX'` (Tracé labs ancré / prolongé) dans la liste
|
||
`curves` sont SKIPPÉES de la boucle de légende (légendes dédiées à
|
||
part) — même bug qu'Android : le catchall (TFS) imprimerait la
|
||
légende TFS en doublon. Quand une liste clé→série reçoit une clé,
|
||
grepper chaque consommateur de la liste (légende, stats, styles).
|
||
13. **Imports ESM = noms de modules EXACTS** (v1.5.0) : `labIsSignificant`
|
||
vit dans `pk-calibration.js` (pas `pk-engine.js`) — un import du
|
||
mauvais module = SyntaxError AU CHARGEMENT, l'app entière ne démarre
|
||
pas ; attrapé par `node --check` + l'E2E « aucune erreur console ».
|
||
14. **Parité du portage** (v1.6.0) : une feature Android portée à moitié
|
||
(moteur sans UI, clés i18n sans miroir) ne casse aucun test existant —
|
||
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
|
||
|
||
Structurels (plateforme), pas des bugs :
|
||
|
||
- **Rappels** : uniquement page ouverte (boucle 30 s + Notification API).
|
||
Aucun équivalent d'AlarmManager — un service worker + push exigerait un
|
||
serveur de push (contre la règle « 100 % local, aucun serveur »).
|
||
- **Agenda récurrent** : impossible (pas de CalendarProvider). L'éditeur
|
||
affiche une note explicative ; `calendarEventId` reste null.
|
||
- **Auto-backup journalier** (v1.7.0 Android) : non porté — le navigateur
|
||
ne peut pas écrire périodiquement dans un dossier arbitraire sans
|
||
intervention (pas d'équivalent d'ACTION_OPEN_DOCUMENT_TREE persisté +
|
||
WorkManager ; File System Access API : Chromium seul, et exige une
|
||
re-validation de permission). La donnée est couverte par l'export/import
|
||
MANUEL du backup (même format, interchangeable avec l'Android).
|
||
- **Montre** : hors sujet navigateur.
|
||
- **Fuseau système** : si le navigateur change de fuseau (voyage), les
|
||
labels X suivent le fuseau CHOISI (Paramètres) ou le nouveau fuseau
|
||
système — l'historique des doses reste en epoch ms (aucun effet sur les
|
||
données, contrairement à un LocalDateTime stocké).
|
||
- **localStorage ≈ 5 Mo** : largement suffisant (années de doses/labs) ;
|
||
en cas de quota dépassé, AppLog avalera l'erreur (fail-safe) et il faudra
|
||
un export/import de nettoyage.
|
||
|
||
## 13. Idées d'évolution
|
||
|
||
1. **Service worker de cache offline** (l'app serait installable en PWA) —
|
||
sans push notifications, uniquement cache statique ; ne casse pas « 100 %
|
||
local » (aucun serveur de données).
|
||
2. Import en mode **fusion** (détection de doublons) — miroir de l'idée
|
||
Android §20.
|
||
3. Tooltip au survol du graphique (valeur + date au point).
|
||
4. Export CSV (miroir Android roadmap).
|
||
5. Tests E2E des gestes (drag du pan via `mouse.drag`) — l'équivalent du
|
||
pinch tactile reste hors de portée headless (comme l'émulateur Android,
|
||
§16.ter de la doc mère).
|
||
|
||
---
|
||
*Doc mise à jour le 11 sept. 2026 (web v1.5.0 — sync Android, dépôt séparé
|
||
HormoneTrack-web) — sync Android v1.5.0, 132 tests verts + E2E,
|
||
backup compatible bidirectionnel, historique propre (aucune donnée de
|
||
santée, aucune dette de confidentialité), versions alignées sur
|
||
l'Android.*
|