Files
lifetrack/docs/research/nutrition-sources.md
MeeJayandClaude Opus 5 93f0689c1e Initial import: LifeTrack v1 (santé, vape, finances)
Tracker de vie auto-hébergé : suivi poids/calories/sport avec planning de
pesées, sevrage tabac (vape) avec modèle de coût DIY et économies, et
finances personnelles avec import de relevés bancaires.

Architecture : FastAPI + SQLAlchemy 2.0 + PostgreSQL 16, React 18 + TS +
Vite + Tailwind + ECharts, déploiement Docker Compose. Modules
auto-découverts des deux côtés (pkgutil / import.meta.glob) et framework
de connecteurs à deux voies (importeurs de fichiers + ingestion JSON)
pour brancher de nouvelles sources sans toucher au noyau.

Validé : 292 tests pytest, tsc + vite build, contrat API/web vérifié
contre le schéma OpenAPI, et déploiement Docker réel sur PostgreSQL 16
(28 tables, SPA servie par nginx, wizard de premier démarrage).

Documentation : README.md, docs/GUIDE.md, CONVENTIONS.md, et les
documents de conception et de recherche dans docs/.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 10:48:57 +02:00

32 KiB
Raw Permalink Blame History

Recherche — Sources de données NUTRITION / CALORIES pour LifeTrack

Document de recherche destiné aux agents d'implémentation. Rédigé le 2026-08-13. Langue : prose en français, identifiants de code en anglais (convention projet). Périmètre : ingestion des données alimentaires (repas, calories, macros) dans le module Santé/Fitness.


1. Résumé exécutif

Source Accès Verdict v1
Foodvisor — export in-app / RGPD CSV limité, sur demande, pas d'automatisation Importeur CSV "best effort" + procédure RGPD documentée (import ponctuel d'historique)
Foodvisor — API consommateur N'existe pas Hors périmètre
Foodvisor — Vision API (analyse photo) Produit entreprise, tarif non public, contrat commercial Non viable v1 ; garder un port d'extension photo_analyzer
Foodvisor — via Health Connect Non confirmé (Foodvisor Android se synchronise à Google Fit, pas à Health Connect de façon documentée) Chemin indirect à tester via Health Sync ; ne pas en dépendre
Health Connect NutritionRecord (bridge Android) Complet, structuré, sur l'appareil uniquement (pas d'API cloud) OUI — endpoint REST d'ingestion + app/bridge Android (stratégie cible)
Open Food Facts API Gratuit, ouvert (ODbL), code-barres + recherche OUI — pilier de la saisie manuelle assistée
CIQUAL (ANSES) Téléchargement Excel/XML, licence ouverte OUI — table locale d'aliments génériques français
MyFitnessPal / Cronometer / Yazio — exports CSV Formats variés, semi-documentés Importeurs CSV optionnels (framework générique, mapping par profil)
Saisie manuelle rapide OUI — socle indispensable

Stratégie v1 recommandée : saisie manuelle rapide + recherche produit Open Food Facts (proxy backend + cache) + table CIQUAL locale pour les aliments génériques + framework d'importeurs CSV (Foodvisor, MyFitnessPal, Cronometer) + endpoint REST POST /api/v1/ingest/nutrition consommé par un bridge Health Connect. Un connecteur Foodvisor "direct" reste impossible aujourd'hui faute d'API ; on préserve la possibilité future via l'architecture connecteurs (section 8).


2. Foodvisor (l'app utilisée par l'utilisateur)

2.1 Ce que Foodvisor est (et n'est pas)

Foodvisor (société française, éditeur de l'app io.foodvisor.foodvisor sur Android et id1064020872 sur iOS) est une app grand public de comptage de calories par photo (IA de reconnaissance alimentaire), scan de code-barres et saisie manuelle, avec coaching payant. Il n'existe aucune API consommateur (pas d'OAuth, pas d'endpoint "mes repas", pas de webhook). Toute intégration "live" est donc exclue.

2.2 Export de données utilisateur (in-app + RGPD)

Constats issus de plusieurs sources (guides Nutrola 2025-2026, politique de confidentialité Foodvisor) :

  • Export intégré : dans l'app, Réglages → section Compte → entrée type « Demander mes données » / « Request my data » (l'intitulé et l'emplacement exacts varient selon les versions ; sur le tableau de bord web l'option est sous AccountPrivacyData). L'export est envoyé par e-mail, généralement sous 24-72 h (le SLA de la politique de confidentialité autorise jusqu'à 30 jours).
  • Contenu de l'export (archive ZIP contenant plusieurs CSV, ou CSV/PDF selon les versions) :
    • objectifs et réglages en CSV clé-valeur "à plat" (objectif calorique, répartition des macros, métriques corporelles, niveau d'activité) ;
    • journal alimentaire : une ligne par entrée avec horodatage, créneau de repas (petit-déjeuner/déjeuner/dîner/collation), nom de l'aliment, portion, macros calculées (protéines, glucides, lipides) et calories ;
    • totaux quotidiens calories + macronutriments (parfois limités à une période récente et non à l'historique complet) ;
    • historique de poids saisi manuellement (date + valeur, une ligne par pesée) ;
    • aliments/recettes personnalisés avec leurs macros mais rarement la décomposition en ingrédients.
  • Ce qui manque (confirmé par les guides de migration) : photos et scores de confiance IA, détails des scans code-barres (codes EAN), micronutriments au-delà des macros, horodatages fins au-delà du jour pour les totaux, notes/noms personnalisés, résumés de coaching.
  • Pièges de format observés : horodatages « généralement ISO 8601 mais parfois en heure locale sans offset » ; encodage UTF-8 (Windows-1252 sur d'anciens exports) ; en-têtes potentiellement en français ou en anglais selon la langue du compte. L'importeur devra être tolérant (détection d'encodage, de délimiteur ,/;, et mapping d'en-têtes bilingue).
  • Demande RGPD (article 15, droit d'accès + article 20, portabilité) : Foodvisor est une société française ; contact data@foodvisor.io (contact officiel indiqué dans la politique de confidentialité pour l'exercice des droits d'accès, rectification, effacement, portabilité). Délai légal : 1 mois, prolongeable de 2 mois pour les demandes complexes. Demander explicitement « l'intégralité de mon journal alimentaire au format structuré lisible par machine (CSV ou JSON), incluant les horodatages, portions et valeurs nutritionnelles » — cette voie contourne les limitations du bouton d'export intégré.

Conséquence pour LifeTrack : l'import Foodvisor est un import ponctuel de rattrapage d'historique (one-shot ou trimestriel), pas un flux continu. L'importeur CSV Foodvisor doit être conçu comme un profil de mapping du framework d'import générique (section 7.3), avec un écran de prévisualisation/mapping de colonnes car le format exact n'est pas documenté officiellement et peut changer. Ne pas coder en dur des noms de colonnes : livrer un mapping par défaut + éditeur de mapping.

2.3 Foodvisor Vision API (produit développeur)

  • Produit B2B de Foodvisor : détection d'aliments et calcul nutritionnel à partir d'une photo (items reconnus, calories, macros, estimation des portions). Page produit : https://www.foodvisor.io/en/vision/ ; documentation : https://vision.foodvisor.io/docs (⚠️ lors de nos tests d'août 2026, ce sous-domaine ne résout plus en DNS — signe que le produit est peu maintenu ou réservé aux clients sous contrat).
  • Modèle d'accès : contrat commercial entreprise uniquement. Pas d'inscription self-service, pas de tarif public, pas de free tier, pas de spécification OpenAPI publique ; « les détails d'endpoints et d'authentification sont partagés avec les clients » sous accord. Accès via le formulaire de contact commercial.
  • Verdict : inutilisable pour un projet personnel auto-hébergé en v1. Si l'analyse de photos de repas devient une exigence, alternatives à évaluer plus tard (non recherchées en détail ici, à vérifier avant usage) : LogMeal API, Passio Nutrition-AI, ou un LLM multimodal généraliste. Décision d'architecture : définir côté backend une interface PhotoAnalyzer (entrée : image ; sortie : liste de FoodItemCandidate avec macros et confiance) pour brancher n'importe quel fournisseur plus tard, y compris Foodvisor Vision si un contrat devenait accessible.

2.4 Foodvisor → Health Connect ?

  • Sur iOS, Foodvisor se synchronise avec Apple Health (non pertinent ici, utilisateur Android).
  • Sur Android, la seule intégration documentée est Google Fit (import/export nutrition, exercice, poids vers l'app Google Fit ; des fils de support Google Fit confirment cette synchro et ses ratés). Aucune mention publique de support Health Connect par Foodvisor à date (recherches août 2026).
  • ⚠️ Les API Google Fit sont dépréciées (annonce Google mai 2024, arrêt programmé, migration officielle vers Health Connect). Le chemin Foodvisor → Google Fit est donc fragile et sans avenir.
  • Chemin indirect à tester (effort ~30 min, aucune dépendance de code) : l'app tierce Health Sync (nl.appyhapps.healthsync, payante ~3-4 €/an) sait recopier des données entre Google Fit et Health Connect pour certains types de données. À tester sur le téléphone de l'utilisateur : si les repas Foodvisor poussés dans Google Fit peuvent être recopiés en NutritionRecord Health Connect, alors le bridge Health Connect de LifeTrack (section 3) récupérera les données Foodvisor sans aucun code spécifique Foodvisor. Ne pas bloquer la v1 sur ce test ; c'est un bonus.

3. Health Connect — type de données Nutrition (chemin cible)

3.1 Principe et contrainte fondamentale

Health Connect (Android 14+ intégré ; app système) est un magasin de données santé sur l'appareil uniquement : il n'existe AUCUNE API cloud/REST côté serveur. Pour amener les données dans LifeTrack, il faut une app Android (le « bridge » déjà prévu au projet) qui lit Health Connect via androidx.health.connect:connect-client et pousse vers l'endpoint REST authentifié de LifeTrack.

3.2 NutritionRecord — champs exacts

Classe : androidx.health.connect.client.records.NutritionRecord (miroir plateforme : android.health.connect.datatypes.NutritionRecord). Un enregistrement = un repas/une prise alimentaire sur un intervalle de temps.

  • Temporalité : startTime, endTime (Instant, UTC), startZoneOffset, endZoneOffset (nullable).
  • Identité : name (String?, nom du repas/aliment), mealType (Int) avec valeurs : MEAL_TYPE_UNKNOWN = 0, MEAL_TYPE_BREAKFAST = 1, MEAL_TYPE_LUNCH = 2, MEAL_TYPE_DINNER = 3, MEAL_TYPE_SNACK = 4.
  • Énergie : energy, energyFromFat — type Energy (exposer en kcal via energy.inKilocalories).
  • Nutriments (tous de type Mass, à lire en grammes via inGrams ; tous nullables) : protein, totalCarbohydrate, sugar, dietaryFiber, totalFat, saturatedFat, unsaturatedFat, monounsaturatedFat, polyunsaturatedFat, transFat, cholesterol, sodium, potassium, calcium, iron, magnesium, phosphorus, zinc, copper, manganese, selenium, iodine, chromium, molybdenum, chloride, caffeine, biotin, folate, folicAcid, niacin, pantothenicAcid, riboflavin, thiamin, vitaminA, vitaminB6, vitaminB12, vitaminC, vitaminD, vitaminE, vitaminK.
    • ⚠️ Les documentations Google confirment : tous les nutriments sont en grammes (y compris sodium — pas en mg — et les vitamines), l'énergie en kcal. Convertir côté bridge ou côté API, mais stocker en unités SI cohérentes (voir 7.2).
  • Métadonnées (metadata) : id (UUID Health Connect, clé de déduplication idéale), dataOrigin.packageName (app source, ex. com.myfitnesspal.android, permet le champ source_app), lastModifiedTime, clientRecordId/clientRecordVersion, recordingMethod, device.
  • Agrégats disponibles (API aggregate) : NutritionRecord.ENERGY_TOTAL, PROTEIN_TOTAL, TOTAL_CARBOHYDRATE_TOTAL, TOTAL_FAT_TOTAL, etc. — utile si le bridge veut pousser des totaux journaliers en plus des repas unitaires. Recommandation : pousser les enregistrements unitaires et laisser LifeTrack agréger.

3.3 Permissions et limites côté bridge

  • Permission de lecture : android.permission.health.READ_NUTRITION (+ autres types déjà prévus : steps, calories, distance, poids, exercices).
  • Limite des 30 jours : par défaut une app ne peut lire que les données antérieures de 30 jours max à la première autorisation. La permission android.permission.health.READ_HEALTH_DATA_HISTORY (introduite en 2024) lève cette limite — à demander dès la v1 du bridge pour récupérer l'historique complet.
  • Lecture incrémentale : utiliser l'API Changes / changement de jetons (getChangesToken + getChanges) pour ne pousser que les deltas ; repli : requête par TimeRangeFilter depuis le dernier push réussi.
  • Lecture en arrière-plan : permission READ_HEALTH_DATA_IN_BACKGROUND + WorkManager périodique (les bridges existants utilisent des périodes de 1-2 h).

3.4 Bridges open source réutilisables (au lieu de tout écrire)

  • HCGateway (github.com/ShuchirJ/HCGateway) : bridge REST universel Health Connect ↔ serveur, app Android + serveur auto-hébergeable, sync bidirectionnelle, push toutes les ~2 h. Projet jeune (« API may change without notice ») mais la partie app Android est une excellente base de code de référence (lecture de tous les types de records dont nutrition, sérialisation JSON).
  • health-connect-webhook (github.com/mcnaveen/health-connect-webhook) : app Android qui pousse les données Health Connect vers un webhook arbitraire — modèle exactement aligné avec notre endpoint d'ingestion générique.
  • Health Sync (payant, closed source) : pour la recopie inter-apps sur le téléphone (cf. 2.4), pas pour pousser vers LifeTrack.

Recommandation : concevoir l'endpoint d'ingestion LifeTrack (section 7.4) de façon à pouvoir accepter soit notre futur bridge maison, soit un HCGateway/webhook adapté, en gardant un schéma JSON proche du modèle NutritionRecord.


4. Open Food Facts (OFF) — recherche produit + code-barres

Base collaborative mondiale (>3 M produits, très bonne couverture des produits vendus en France). Gratuit, sans clé d'API.

4.1 Endpoints

  • Produit par code-barres : GET https://world.openfoodfacts.org/api/v2/product/{barcode} (JSON). Utiliser https://fr.openfoodfacts.org/... pour prioriser les libellés français. Paramètre fields pour limiter la réponse, ex. : ?fields=code,product_name,product_name_fr,brands,quantity,serving_size,serving_quantity,nutriments,nutriscore_grade,nova_group,ecoscore_grade,image_front_small_url,categories_tags,lang
  • API v3 (courante, recommandée pour les nouvelles intégrations) : GET /api/v3/product/{barcode} — v2 reste supportée ; v0/v1 à éviter.
  • Recherche plein texte : ⚠️ pas disponible dans l'API v2 serveur. Deux options :
    • Search-a-licious (service de recherche officiel) : https://search.openfoodfacts.org (API documentée sur ce domaine, GET /search?q=...&langs=fr&page_size=20) — recommandé ;
    • legacy : GET https://fr.openfoodfacts.org/cgi/search.pl?search_terms=...&json=1 (lent, déprécié mais fonctionnel).
  • Recherche filtrée v2 (par catégorie/marque/nutriment, sans plein texte) : GET /api/v2/search?categories_tags=...&fields=....
  • Environnement de staging : https://world.openfoodfacts.net (HTTP Basic off/off) — utiliser pour les tests d'intégration automatisés.
  • SDK Python officiel : paquet PyPI openfoodfacts (openfoodfacts-python) — utilisable directement dans l'API FastAPI, mais un simple client httpx suffit.

4.2 Champs nutriments utiles (clés JSON exactes)

Par 100 g : energy-kcal_100g, energy_100g (kJ), proteins_100g, carbohydrates_100g, sugars_100g, fat_100g, saturated-fat_100g, fiber_100g, salt_100g, sodium_100g (en g). Variantes par portion : suffixe _serving (ex. energy-kcal_serving) + serving_size (texte, ex. "30 g") et serving_quantity (nombre). Les champs peuvent être absents ou incohérents (données collaboratives) : toujours valider (energy-kcal_100g manquant → recalculer depuis kJ ÷ 4,184 ; contrôler que macros × facteurs Atwater ≈ énergie à ±20 %).

4.3 Règles d'usage impératives

  • User-Agent personnalisé obligatoire : format AppName/Version (ContactEmail), ex. LifeTrack/1.0 (meejayproduction@gmail.com) — sous peine d'être traité comme bot.
  • Rate limits (docs officielles, août 2026) : 15 req/min/IP pour les lectures produit, 10 req/min/IP pour les recherches. Conséquences d'implémentation : (a) toutes les requêtes OFF passent par le backend LifeTrack (proxy), jamais depuis le navigateur ; (b) cache local de chaque produit consulté (table food_items, cf. 7.2) — un produit scanné une fois ne re-sollicite plus OFF ; (c) throttle côté serveur (token bucket 10/min) + retry avec backoff sur 429.
  • Pas de scraping massif via l'API : pour un besoin hors-ligne complet, utiliser les exports intégraux (CSV ~ plusieurs Go, JSONL, dump MongoDB, Parquet sur Hugging Face) — non nécessaire en v1.
  • Licence : base sous ODbL (contenus sous DbCL, images CC-BY-SA). Pour un usage personnel auto-hébergé : afficher une attribution « Données produits : Open Food Facts (ODbL) » dans l'UI suffit largement ; l'obligation de partage à l'identique ne s'applique que si l'on redistribue une base dérivée.

5. CIQUAL (ANSES) — table de composition française (aliments génériques)

Complément indispensable d'OFF : OFF couvre les produits industriels code-barrés, CIQUAL couvre les aliments génériques (« Pomme, crue », « Baguette courante », « Poulet rôti, cuisse ») — exactement ce qu'il faut pour la saisie manuelle de plats maison.

  • Éditeur : ANSES (Agence nationale de sécurité sanitaire de l'alimentation). Site de consultation : https://ciqual.anses.fr.
  • Versions : table Ciqual 2020 (~3 185 aliments, ~60 constituants) ; une table Ciqual 2025 a été publiée fin 2025 (~3 484 aliments, sucres individuels détaillés, fruits/légumes mis à jour incl. outre-mer). Prendre la 2025 si disponible au moment de l'implémentation, sinon 2020.
  • Téléchargement : formats Excel (.xls/.xlsx) et XML, depuis ciqual.anses.fr (rubrique téléchargement) et sur data.gouv.fr : jeu de données « Table de composition nutritionnelle des aliments Ciqual » (https://www.data.gouv.fr/datasets/table-de-composition-nutritionnelle-des-aliments-ciqual-2020). La table 2025 est aussi déposée sur entrepot.recherche.data.gouv.fr (DOI 10.57745/RDMHWY). Documentation PDF officielle du format Excel disponible sur le site Ciqual.
  • Licence : open data (Licence Ouverte / Etalab 2.0 via data.gouv.fr) — usage libre avec mention de la source « ANSES — Table Ciqual ».
  • Structure du fichier Excel (à connaître pour l'importeur) :
    • une ligne par aliment : alim_code (code numérique stable), alim_nom_fr (libellé français), alim_nom_eng, groupes/sous-groupes (alim_grp_code, alim_grp_nom_fr, alim_ssgrp_...) ;
    • une colonne par constituant, valeurs pour 100 g, libellés du type « Energie, Règlement UE N° 1169/2011 (kcal/100 g) », « Protéines, N x facteur de Jones (g/100 g) », « Glucides (g/100 g) », « Lipides (g/100 g) », « Sucres (g/100 g) », « Fibres alimentaires (g/100 g) », « Sel chlorure de sodium (g/100 g) », vitamines/minéraux ;
    • pièges de parsing : séparateur décimal virgule ; valeurs non numériques "-" (non déterminé), "traces", et bornes "< 0,5" — l'importeur doit normaliser (traces → 0, < x → x/2 ou 0 selon règle choisie, - → NULL) ; encodage à vérifier (exports historiques en Windows-1252/latin-1).
  • Intégration recommandée : script one-shot scripts/import_ciqual.py qui charge le fichier Excel/XML dans la table food_items avec source='ciqual', source_id=alim_code. Le fichier CIQUAL est mis dans un volume/dossier data/ (préchargé dans l'image ou téléchargé au premier lancement par l'admin) — pas d'appel réseau au runtime.

6. Exports CSV des autres apps de suivi alimentaire

Objectif : le framework d'import CSV doit accepter les exports des apps majeures, pour migration d'historique ou si l'utilisateur change d'app un jour.

6.1 MyFitnessPal

  • Export "File Export" (Premium uniquement) : Web → Reports/Settings → export par plage de dates ; production asynchrone (minutes à heures), lien envoyé par e-mail ; 3 CSV : Nutrition, Exercise, Progress. Le CSV Nutrition est à la granularité jour × repas (Breakfast/Lunch/Dinner/Snacks), avec colonnes type : Date, Meal, Calories, Fat (g), Saturated Fat, Polyunsaturated Fat, Monounsaturated Fat, Trans Fat, Cholesterol, Sodium (mg), Potassium, Carbohydrates (g), Fiber, Sugar, Protein (g), Vitamin A, Vitamin C, Calcium, Iron, Note (⚠️ liste constatée sur des exports réels et outils communautaires, non documentée officiellement — valider sur un échantillon réel avant de figer le mapping ; d'où l'éditeur de mapping, 7.3).
  • Compte gratuit : demande de téléchargement des données personnelles (RGPD/CCPA) via les réglages du compte — archive moins structurée.
  • Il existe une lib Python non officielle python-myfitnesspal (scraping avec login) — fragile, hors périmètre v1.

6.2 Cronometer

  • Export self-service (Web → More/AccountExport Data, plage de dates), immédiat et gratuit, en 5 fichiers CSV : servings.csv (une ligne par aliment consommé : Day, Group (repas), Food Name, Amount, puis ~70 colonnes de nutriments avec unités dans l'en-tête, ex. Energy (kcal), Protein (g), Sodium (mg)), dailysummary.csv (totaux/jour), exercises.csv, biometrics.csv (poids !), notes.csv. Le plus propre et le plus riche des trois — profil de mapping facile.

6.3 Yazio

  • Pas d'export CSV natif : l'app ne propose qu'un PDF limité ; l'archive complète s'obtient par demande RGPD (siège à Erfurt, Allemagne ; réponse sous 1 mois ; en pratique ZIP de JSON/CSV).
  • Outils open source non officiels : github.com/aleksandr-bogdanov/yazio-exporter (Python ; login e-mail/mot de passe sur l'API non officielle de Yazio, export JSON/CSV/SQLite : journal, aliments consommés, poids, exercices, eau, 40+ micronutriments) et github.com/tobintax/yazio-csv-exporter. Utilisables ponctuellement par l'utilisateur en dehors de LifeTrack ; LifeTrack se contente d'importer les CSV produits.

6.4 Foodvisor (rappel)

Voir 2.2 : ZIP de CSV (journal par entrée, totaux quotidiens, poids, réglages clé-valeur). Profil d'import dédié, tolérant (encodage, délimiteur, en-têtes FR/EN, horodatages sans offset → interpréter en Europe/Paris puis convertir en UTC).


7. Stratégie d'ingestion nutrition v1 — recommandations concrètes

7.1 Les quatre canaux v1 (ordre de priorité d'implémentation)

  1. Saisie manuelle rapide (UI français) : formulaire "repas" minimal — date/heure (défaut : maintenant, TZ Europe/Paris), type de repas (petit-déjeuner/déjeuner/dîner/collation), soit saisie libre kcal+macros, soit sélection d'un aliment (recherche locale food_items + OFF + CIQUAL) × quantité. Duplication d'un repas précédent ("répéter hier") = fonctionnalité à fort ROI.
  2. Recherche produit : endpoint backend GET /api/v1/foods/search?q=... qui interroge (a) le cache local food_items, (b) CIQUAL local, (c) Open Food Facts (Search-a-licious + fallback code-barres direct), fusionne et met en cache. Scan de code-barres possible plus tard côté PWA (caméra) → GET /api/v1/foods/barcode/{ean}.
  3. Importeurs CSV via le framework d'import générique du projet : profils foodvisor, myfitnesspal, cronometer (+ generic_nutrition_csv avec mapping manuel). Pipeline : upload → détection encodage/délimiteur → proposition de mapping (profil) → prévisualisation → validation → insertion idempotente.
  4. Endpoint d'ingestion REST pour le bridge Health Connect (voir 7.4) — même endpoint générique que pour les pas/poids, avec le type nutrition.

7.2 Modèle de données proposé (SQLAlchemy, PostgreSQL)

Deux tables, séparant référentiel d'aliments et journal :

food_items                       # referential/cache of foods
  id                UUID PK
  source            TEXT NOT NULL         -- 'off' | 'ciqual' | 'custom'
  source_id         TEXT                  -- OFF barcode | ciqual alim_code | NULL
  name              TEXT NOT NULL         -- French label preferred
  brand             TEXT
  energy_kcal_100g  NUMERIC
  protein_g_100g    NUMERIC
  carbs_g_100g      NUMERIC
  sugar_g_100g      NUMERIC
  fat_g_100g        NUMERIC
  sat_fat_g_100g    NUMERIC
  fiber_g_100g      NUMERIC
  salt_g_100g       NUMERIC              -- salt = sodium * 2.5
  serving_size_g    NUMERIC
  raw_payload       JSONB                 -- full OFF/CIQUAL record
  UNIQUE (source, source_id)

nutrition_entries                # the journal (one row = one food eaten OR one whole meal)
  id                UUID PK
  user_id           UUID FK
  eaten_at          TIMESTAMPTZ NOT NULL  -- stored UTC
  meal_type         TEXT NOT NULL         -- 'breakfast'|'lunch'|'dinner'|'snack'|'unknown'
  label             TEXT                  -- free-text name
  food_item_id      UUID FK NULL          -- when picked from referential
  quantity_g        NUMERIC NULL
  energy_kcal       NUMERIC NOT NULL      -- always denormalized at entry level
  protein_g         NUMERIC
  carbs_g           NUMERIC
  sugar_g           NUMERIC
  fat_g             NUMERIC
  sat_fat_g         NUMERIC
  fiber_g           NUMERIC
  sodium_mg         NUMERIC
  source            TEXT NOT NULL         -- 'manual'|'import:foodvisor'|'import:mfp'|'import:cronometer'|'healthconnect'
  source_record_id  TEXT                  -- HC metadata.id, or import row hash
  source_app        TEXT                  -- HC dataOrigin.packageName
  import_batch_id   UUID FK NULL
  UNIQUE (user_id, source, source_record_id)
  • Unités stockées : kcal, grammes, sodium en mg (convention affichage FR) — conversions faites à l'ingestion (Health Connect fournit tout en g/kcal, cf. 3.2).
  • Le bilan énergétique quotidien (kcal in) = SUM(energy_kcal) groupé par jour en Europe/Paris (conversion au moment de la requête, pas au stockage).
  • Déduplication inter-sources : contrainte unique (user_id, source, source_record_id) pour l'idempotence intra-source ; pour l'inter-sources (ex. un repas présent à la fois dans le CSV Foodvisor et via Health Connect), règle v1 simple : l'UI signale les jours où deux sources se chevauchent (>1 source sur la même journée) et propose de masquer une source par plage de dates (source_priority par jour). Ne pas tenter de matching flou par entrée en v1.

7.3 Framework d'import CSV — exigences pour la nutrition

  • Détection : BOM/UTF-8/Windows-1252, délimiteur ,/;/tab, format de date (YYYY-MM-DD, DD/MM/YYYY, ISO 8601 avec/sans offset — sans offset ⇒ supposer Europe/Paris).
  • Profils = fichiers de mapping déclaratifs (JSON/YAML versionnés dans le repo) : column → field, transformations (comma_decimal, meal_type_map FR/EN : Petit-déjeuner→breakfast, etc.), granularité (per_food_row pour Cronometer servings.csv, per_meal_row pour MFP, per_entry_row pour Foodvisor).
  • Idempotence : source_record_id = SHA-256(user_id + source + date + meal + label + kcal) en absence d'ID natif ; ré-import du même fichier = 0 doublon.
  • Chaque import crée un import_batch (annulable en bloc — indispensable vu l'instabilité des formats).

7.4 Endpoint d'ingestion Health Connect (bridge)

POST /api/v1/ingest/nutrition (JWT device token, scope ingest) — schéma aligné sur NutritionRecord :

{
  "records": [
    {
      "source_record_id": "hc-uuid-from-metadata-id",
      "source_app": "io.foodvisor.foodvisor",
      "start_time": "2026-08-13T11:45:00Z",
      "end_time": "2026-08-13T12:15:00Z",
      "meal_type": "lunch",
      "name": "Salade poulet",
      "energy_kcal": 620.0,
      "protein_g": 42.1,
      "total_carbohydrate_g": 51.0,
      "sugar_g": 8.2,
      "total_fat_g": 24.3,
      "saturated_fat_g": 6.1,
      "dietary_fiber_g": 7.0,
      "sodium_g": 1.1
    }
  ]
}

Réponse : {"accepted": n, "duplicates": m, "rejected": [...]} — upsert sur (user_id, 'healthconnect', source_record_id). Le même pattern (/ingest/steps, /ingest/weight, ...) sert tout le bridge Health Connect. Côté bridge : demander READ_NUTRITION + READ_HEALTH_DATA_HISTORY, sync incrémentale par Changes API, batch de 100-500 records.

7.5 Garder un futur connecteur Foodvisor possible

  1. Architecture connecteurs : chaque source = classe Connector (métadonnées, capacités file_import/api_pull/push, mapping). FoodvisorCsvConnector (v1, file_import) et un éventuel FoodvisorApiConnector (si une API apparaît) partagent le même normaliseur FoodvisorRecordNormalizer.
  2. Interface PhotoAnalyzer (cf. 2.3) : port d'extension pour Foodvisor Vision ou tout autre service d'analyse de photo.
  3. Chemin sans code : si le test Health Sync (2.4) réussit, les données Foodvisor arrivent déjà par le canal Health Connect avec source_app identifiable.
  4. Veille : re-vérifier périodiquement (a) l'apparition d'un support Health Connect dans l'app Foodvisor Android (fiche Play Store, permissions android.permission.health.WRITE_NUTRITION), (b) la résurrection de vision.foodvisor.io.

8. Checklist pour les agents d'implémentation

  • Table food_items + import CIQUAL (script one-shot, gestion traces/< x/virgule décimale).
  • Proxy OFF : client httpx avec User-Agent: LifeTrack/1.0 (meejayproduction@gmail.com), cache DB, throttle 10 req/min, staging .net pour les tests.
  • GET /foods/search, GET /foods/barcode/{ean} ; UI de saisie manuelle FR (types de repas : Petit-déjeuner, Déjeuner, Dîner, Collation).
  • Framework import CSV + profils cronometer (le plus simple, commencer par lui), myfitnesspal, foodvisor, generic_nutrition_csv ; éditeur de mapping ; batches annulables.
  • POST /api/v1/ingest/nutrition idempotent (+ doc French pour configurer HCGateway/webhook en attendant le bridge maison).
  • Attributions UI : « Open Food Facts (ODbL) », « ANSES — Table Ciqual ».
  • Doc utilisateur FR : procédure d'export Foodvisor in-app + modèle d'e-mail RGPD à data@foodvisor.io.

9. Sources