Siphonight 491efef8eb v1.4.10 — portage initial de l'app Android v1.4.10
Portage navigateur complet de l'app Android HormoneTrack v1.4.10 :
- moteur PK fidèle (Estrannaise tables ODS, Transfem Science V3C,
  WHSAH fit Mona, Bateman) — paramètres identiques, invariants Android
  préservés (fixes #19-#23, #35, #52-#62), 121 tests Node épinglés
- calibration par période d'ester et par modèle affiché (#60/#61)
- UI 6 écrans + éditeur, CurveChart Canvas (zoom/pan fractionnaire,
  prévision, pics/creux, fuseau configurable), rappels web
  (Notification API), seuils d'alerte, i18n FR/EN
- sauvegarde JSON v2 compatible Android bidirectionnelle (rétrocompat v1)
- 100 % local : localStorage, aucun serveur applicatif, aucune télémétrie
- processus : scripts/check.sh (syntaxe, i18n, tests, E2E navigateur,
  smoke HTTP), docs séparées (README + DEVELOPPEMENT + CHANGELOG)
- dépôt GIT SÉPARÉ de l'Android : historique 100 % propre, versions
  alignées sur l'Android porté, changelogs indépendants

Non porté (impossible dans un navigateur, documenté §12) : agenda
récurrent, notifications onglet fermé, montre.
2026-09-08 14:34:58 +02:00

157 lines
6.5 KiB
JavaScript

/**
* ─────────────────────────────────────────────────────────────────────────────
* Sauvegarde JSON (portage de `data/backup/BackupManager.kt`).
*
* ⚠️ COMPATIBILITÉ BIDIRECTIONNELLE avec les exports de l'app Android :
* format BackupData v2 —
*
* {
* "version": 2,
* "exportedAt": 1749...,
* "treatments": [ Treatment... (champs Gson/Kotlin, cf data/models.js) ],
* "doseLogs": [ DoseLog... ],
* "labResults": [ LabResult... ],
* "tConfig": { "base": 6.0, "floor": 0.2, "k": 0.19 },
* "settings": { "language": ..., "autoCalibrate": ...,
* "alertE2High": ..., "alertE2Low": ...,
* "alertTHigh": ..., "alertTLow": ... } ← optionnel (v2)
* }
*
* RÉTROCOMPATIBILITÉ : les backups v1 (SANS `settings`) restent parsables
* (settings = null) et importables — l'import ne vérifie pas strictement la
* version (miroir du comportement Gson côté Android). `changelog_seen_version`
* est volontairement EXCLU des settings d'export (pas une donnée utile à
* restaurer — cf BackupManager.kt).
*
* PUR (DOM-free) → testable en Node (web/tests/backup.test.js, y compris
* round-trip avec un vrai export Android si local-test-data/ est présent).
* ─────────────────────────────────────────────────────────────────────────────
*/
/**
* Construit le JSON de sauvegarde (BackupData v2, mêmes noms de champs que
* Gson).
*
* @param {object} collections { treatments, doseLogs, labResults } (store.exportAll)
* @param {object} tConfig { base, floor, k }
* @param {object|null} [userSettings=null] Réglages à embarquer (v2) —
* SEULS les champs du schéma UserSettings Android sont sérialisés (champs
* plats), jamais les clés web extra (chartTimezone, alertNotifiedState…) :
* un backup importé dans l'app Android ne doit pas contenir de clés
* inconnues (Gson les ignorerait, mais autant rester stricts).
* @returns {string} JSON
*/
export function buildBackupJson(collections, tConfig, userSettings = null) {
const data = {
version: 2,
exportedAt: Date.now(),
treatments: collections.treatments,
doseLogs: collections.doseLogs,
labResults: collections.labResults,
tConfig: {
base: tConfig.base,
floor: tConfig.floor,
k: tConfig.k,
},
};
if (userSettings) {
// Champs PLATS volontairement (miroir UserSettings.kt — Gson lit par
// réflexion côté Android : pas de nested inconnu)
data.settings = {
language: userSettings.language ?? null,
autoCalibrate: userSettings.autoCalibrate ?? null,
alertE2High: userSettings.alertE2High ?? null,
alertE2Low: userSettings.alertE2Low ?? null,
alertTHigh: userSettings.alertTHigh ?? null,
alertTLow: userSettings.alertTLow ?? null,
};
}
return JSON.stringify(data);
}
/**
* Parse un backup (Android v1 ou v2, ou export web) en structure validée.
*
* Les listes manquantes deviennent vides, les types numériques sont
* normalisés (Gson accepte des entiers pour des Double) — l'import ne
* vérifie pas strictement la version, comme côté Android.
*
* @param {string} json
* @returns {{version:number, exportedAt:number|null,
* treatments:object[], doseLogs:object[], labResults:object[],
* tConfig:{base:number,floor:number,k:number},
* settings:object|null}}
* @throws {Error} si le JSON est illisible ou n'a pas la forme d'un backup
*/
export function parseBackupJson(json) {
let raw;
try {
raw = JSON.parse(json);
} catch (e) {
throw new Error(`JSON illisible : ${e.message}`);
}
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
throw new Error('Le fichier ne contient pas un backup HormoneTrack');
}
if (!('treatments' in raw) && !('doseLogs' in raw) && !('labResults' in raw)) {
throw new Error('Aucune donnée de backup reconnue (treatments/doseLogs/labResults absents)');
}
return {
version: typeof raw.version === 'number' ? raw.version : 1,
exportedAt: typeof raw.exportedAt === 'number' ? raw.exportedAt : null,
treatments: Array.isArray(raw.treatments) ? raw.treatments : [],
doseLogs: Array.isArray(raw.doseLogs) ? raw.doseLogs : [],
labResults: Array.isArray(raw.labResults) ? raw.labResults : [],
// Défauts TConfig.kt si absent (backup tronqué) — comme Gson
// reconstruirait TConfig() par défaut
tConfig: {
base: numOrNull(raw.tConfig && raw.tConfig.base) ?? 6.0,
floor: numOrNull(raw.tConfig && raw.tConfig.floor) ?? 0.2,
k: numOrNull(raw.tConfig && raw.tConfig.k) ?? 0.19,
},
// settings absent = backup v1 → null (l'appelant ne touche pas aux réglages)
settings: raw.settings && typeof raw.settings === 'object' ? raw.settings : null,
};
}
/** @private Convertit en nombre si possible, sinon null. */
function numOrNull(v) {
const n = typeof v === 'string' ? Number(v) : v;
return typeof n === 'number' && Number.isFinite(n) ? n : null;
}
/**
* Nom de fichier d'export (miroir d'ExportFileNames.backupFileName Android) :
* `hormonetrack-backup-YYYYMMDD.json` — pattern SANS heure.
*
* Le helper Android existait parce qu'un pattern horaire passé à LocalDate
* crashait l'app (#48) ; côté web, `new Date()` porte tout — le helper reste
* centralisé par discipline (une seule source pour le nom).
*
* @param {Date} [now=new Date()]
* @returns {string}
*/
export function backupFileName(now = new Date()) {
const y = now.getFullYear();
const m = String(now.getMonth() + 1).padStart(2, '0');
const d = String(now.getDate()).padStart(2, '0');
return `hormonetrack-backup-${y}${m}${d}.json`;
}
/**
* Nom de fichier d'export des logs de diagnostic (miroir
* ExportFileNames.diagnosticLogFileName) : `hormonetrack-logs-YYYYMMDD-HHmm.txt`
* — pattern AVEC heure (le fichier Android documente pourquoi : bug #48).
*
* @param {Date} [now=new Date()]
* @returns {string}
*/
export function diagnosticLogFileName(now = new Date()) {
const y = now.getFullYear();
const mo = String(now.getMonth() + 1).padStart(2, '0');
const d = String(now.getDate()).padStart(2, '0');
const h = String(now.getHours()).padStart(2, '0');
const mi = String(now.getMinutes()).padStart(2, '0');
return `hormonetrack-logs-${y}${mo}${d}-${h}${mi}.txt`;
}