- predictedE2 : valeur brute au creux × facteur du ester (auto-calibration
ON requise — sinon rien, une valeur brute serait trompeuse) ; rendu
coloré face à la cible dans labs.js + home.js.
- pooledRegimeDoses : les doses de tous les traitements partageant (ester
effectif, mg) forment UNE séquence — le re-parenting d'historique
(« 6d-old ») ne redémarre plus la stabilisation. Garde-fous : ester ≠
jamais poolé, dose ≠ exclue.
- Cible de creux E2 (opt-in) : store + carte settings + backup
(rétrocompatible — un backup ancien n'efface pas la cible locale) +
i18n FR/EN. ⚠️ DISTINCT des seuils d'alerte : jamais de notification.
- 15 tests (pooling ×3, statut cible, round-trip backup) ; leçon E2E :
bump WEB_VERSION sans section CHANGELOG = dialog vide.
199 tests + E2E verts (check.sh). WEB_VERSION alignée sur 1.13.0.
160 lines
6.7 KiB
JavaScript
160 lines
6.7 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,
|
|
// v1.13.0 : cible de creux (champs optionnels — rétrocompatibles)
|
|
troughTargetLow: userSettings.troughTargetLow ?? null,
|
|
troughTargetHigh: userSettings.troughTargetHigh ?? 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`;
|
|
}
|