Conteneurisation (site 100 % statique — l'image ne détient AUCUNE donnée, vie privée identique) : - Dockerfile : nginxinc/nginx-unprivileged:alpine (uid 101, port 8080, pas de root), healthcheck wget intégré, labels OCI - nginx.conf : no-cache systématique (cohérence du jeu de fichiers à chaque mise à jour d'image, coût nul : ~600 Ko + gzip sur l'asset PK), gzip, en-têtes de sécurité, deny des dotfiles - .dockerignore : runtime uniquement (docs/ inclus — dialog changelog) ; local-test-data exclu en filet de sécurité - docker-compose.yml : read_only + tmpfs (/var/cache/nginx, /run, /tmp — les temporaires de nginx-unprivileged, constat au premier test réel) + no-new-privileges Nouveaux tests avant release : - scripts/container-test.sh : build + run DURCI (config compose) + healthcheck healthy + endpoints 200 + headers (no-cache/nosniff/DENY/ no-referrer) + gzip réel + MIME strict des modules ES + fichiers cachés non servis + image propre (pas de node_modules/npm) + docs/ embarqué + E2E playwright complet CONTRE LE CONTENEUR (HRT_E2E_BASE) - scripts/e2e.mjs : mode externe HRT_E2E_BASE (pilote un site déjà déployé — conteneur inclus, serveur local non démarré) - scripts/check.sh --release : supplée les 9 vérifications (version ↔ changelog ↔ tag ↔ arbre propre + test conteneur si daemon) Runtime Docker local installé pour le CI-like (colima 2 CPU / 2 Go) : premier build + run réel = 2 problèmes testés et corrigés (tmpfs /tmp, key case header_json curl). CONTAINER TEST OK en configuration durcie.
38 KiB
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, docs mère : sondocs/DEVELOPPEMENT.md). Guide utilisateur : ../README.md.
Table des matières
- Contexte & objectifs du portage
- Stack & décisions structurantes
- Deux dépôts séparés : HormoneTrack (APK) ↔ HormoneTrack-web
- Architecture & correspondance fichier par fichier
- Persistance (localStorage) & schéma de données
- Compatibilité des backups avec l'Android
- Fuseaux horaires (Intl)
- Tests — processus complet
- Workflow de développement
- Processus de push & releases (sync Android)
- Bugs potentiels évités pendant le portage
- Limites connues & non-portés
- 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 :
- 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).
- Compatibilité backup bidirectionnelle (schéma
BackupDatav2, champs Gson/Kotlin identiques). - 100 % local : tout dans le navigateur (localStorage), le serveur ne sert que des fichiers statiques.
- 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.)
- 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 121 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é)
- 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.
- 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.
- 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)
├── .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)
│ │ ├── alerts.js Alerts.kt : seuils opt-in, codec d'état,
│ │ │ shouldNotify (anti-spam)
│ │ ├── presets.js PKPresets.kt (22 presets, clés i18n)
│ │ ├── 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, HTTP)
│ ├── i18n-check.mjs cohérence FR/EN + clés utilisées
│ └── e2e.mjs E2E playwright (Firefox headless, ?demo=1)
├── 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)
│ ├── 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 dedata/,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
autoCalibratedpar 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.nowMsest 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. - Le graphique est purement déclaratif : pan/zoom vivent dans
chart.js(parent),chart-canvas.jsne 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). calendarEventIdexiste dans les objets (fidélité du schéma) mais reste TOUJOURSnullcôté web.
6. Compatibilité des backups avec l'Android
Format BackupData v2 — verrouillé par des tests des deux côtés :
{
"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(miroirUserSettings.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
tConfiget les réglages du backup sont restaurés comme sur l'Android. tests/backup.test.jsimporte les vrais exports Android viahelpers.localTestDataDir()(dépôt Android voisin, skip si absent — comportementAssumeidentique 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(...)(viaIntl.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 :
Intlrenvoiehour: '24'avech24et pash23— toujourshourCycle: 'h23';- un formatter avec SEULEMENT
minutene 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 |
121 tests : noyau PK, calibration, backup, store, helpers, alertes, rappels, changelog | 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)
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.
- 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
- Le code visé doit être dans
js/pk/(pur) ou testable via le store (backend Map) — l'UI est couverte par l'E2E. - Réutiliser
tests/helpers.js(makeTreatment/makeDose/makeLab,initProfiles,assertClose). - ⚠️ Les fixtures ont des defaults identiques aux entités Kotlin :
id: 0= « pas en base » (unid: 1par défaut a faussé trois tests pendant le portage — cf §11). - 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.pysur 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) ; - 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é parrenderRouteune fois l'asset chargé) — attendre ce sélecteur, jamais unsleepnu. - ⚠️ Le seed de langue (
addInitScript) est conditionnel : l'écraser à chaque navigation effaceraitchangelogSeenVersionet ferait réapparaître le dialog à chaquepage.goto(une goto = reboot complet de la SPA).
9. Workflow de développement
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/@returnset, 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.mjsbloque sinon) — leçon Android §12. - Champs de modèles : ne JAMAIS renommer un champ de
data/models.jssans vérifierBackupManager.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). La publication Gitea (release avec assets) du côté web est GELÉE tant que la qualité n'est pas validée au standard APK : en attendant, une release web = commit + tag UNIQUEMENT.
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)
- Android (doc Android §16 checklist complète) : bump
versionName/versionCode, tests + lint verts, sectiondocs/CHANGELOG.md, commit, tag annotévX.Y.Z. - Web (cette doc) :
a. porter la/les features si pas déjà fait (commits par couche) ;
b.
WEB_VERSION = "X.Y.Z"dansjs/ui/settings.js= même numéro que l'Android + section## [X.Y.Z]en tête dedocs/CHANGELOG.md(le dialog « Nouveautés » la lira) ; c.bash scripts/check.shvert (E2E inclus) ; d. commitvX.Y.Z — résumé du portage+ tag annotévX.Y.Z. - Push des deux dépôts :
⚠️ Ne JAMAIS comparer les versions en chaînes (piège #38 Android) — tout script futur passe par une comparaison numérique.# 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 - Releases Gitea :
- Android :
publish-release.pysur les deux instances (2 APK), cf doc Android §16.bis ; - Web : GELÉ tant que la parité qualité n'est pas validée — sinon
créer la release avec le corps = section
docs/CHANGELOG.mdet, si un jour on distribue un zip, le vérifier par téléchargement (leçon #43 Android : une invocation fait tout, les uploads rapprochés se remplacent mutuellement).
- Android :
- 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)
bash scripts/check.shvert (E2E inclus si installé) ;- 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-datavide si le dossier local existe) ; - 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).
# ── 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) :
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) : fichiers + docs prêts et relus — build NON
exécuté localement (aucun daemon Docker sur la machine de dev, CLI seule) ;
la configuration compose (read_only/tmpfs) est le pattern standard de
nginx-unprivileged, à valider au premier docker compose up sur l'hôte
cible. 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, cf plus haut).
11. Bugs potentiels évités pendant le portage
Trouvés pendant l'écriture (à ne pas réintroduire) :
- Propriétés dérivées Kotlin = fonctions JS :
usesProfileModelest un getter en Kotlin mais n'existe pas sur les objets JSON plats → le moteur retombait silencieusement en Bateman (courbes ÷100). Fix :usesProfileModel(treatment)danspk-engine.js. Toute propriété dérivée d'entité (isInjection, …) doit être recalculée, jamais lue. - Fixtures de test :
makeTreatmentavecid: 1par défaut (au lieu de 0 = « pas en base ») faisait échouer auto-increment/CASCADE. Defaults des helpers == valeurs d'insertion Kotlin. - 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.
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).-0:Math.trunc/négation produisent-0;assert.equal(-0, 0)échoue en strict — normaliser dans les helpers (panDeltaHours,clampPanHours).- Imports relatifs :
app.jsvit dansjs/ui/— un import écrit comme depuisjs/donne des 404 MIME silencieuses (le module root ne charge PAS, aucune erreur fatale visible). D'où le checknode --check+ l'E2E « aucune erreur console ». - Exports du barrel :
pk/index.jsdoit ré-exporter tout ce que les écrans importent nommément (pointHoursBefore, …) — une omission = SyntaxError au chargement. L'E2E l'attrape (page vide). - Assignation à une variable non déclarée en ESM (strict) = crash du
module entier (
lastRoutedéclaré dansmain()mais assigné dansrenderRoute()) → remonter l'état partagé au niveau module. - Closures de dialog :
const dlg = showDialog({ actions: [… dlg.close()] })— oublier leconstdonne une ReferenceError AU CLIC (pas au chargement), invisible sans test d'interaction. L'E2E clique maintenant les actions. - Seed de test conditionnel : un
addInitScriptnon conditionnel écrasesettingsà chaque navigation (re-lecture du changelog à chaque goto). Cf §8. getSettings()fusionne les défauts : un stockage partiel ne doit jamais exposer des champsundefined(les gardes!== nulldeviennent fausses autrement).
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 ;
calendarEventIdreste null. - 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
- 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).
- Import en mode fusion (détection de doublons) — miroir de l'idée Android §20.
- Tooltip au survol du graphique (valeur + date au point).
- Export CSV (miroir Android roadmap).
- 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 8 sept. 2026 (web v1.4.10, dépôt séparé HormoneTrack-web) — portage de l'Android v1.4.10, 121 tests verts + E2E, backup compatible bidirectionnel, historique propre (aucune donnée de santée, aucune dette de confidentialité), versions alignées sur l'Android.