# LifeTrack — CONVENTIONS > Extrait de docs/design/architecture.md §10 — règles obligatoires pour tout développeur (humain ou agent). ### C1. Langues - **Code** (identifiants, fichiers, tables, colonnes, routes API, commentaires, messages de commit) : **anglais**. - **UI et messages d'erreur destinés à l'utilisateur** (`AppError(message=...)`, labels, `strings.ts`, docs utilisateur) : **français**, ponctuation française correcte, pas d'anglicismes gratuits. Formats d'affichage `fr-FR` (virgule décimale, espace insécable des milliers, `€` après le montant). ### C2. Créer un module backend 1. Créer `apps/api/app/modules//` avec `__init__.py` (vide) et **obligatoirement** `router.py` exposant `router = APIRouter(prefix="/", tags=[""])`. Fichiers standard : `models.py`, `schemas.py`, `service.py` ; optionnels : `importers.py`, `ingest.py`, `calculations.py`. 2. **Ne jamais modifier** `main.py`, `module_loader.py`, ni le module d'un autre chantier. L'enregistrement est automatique (pkgutil) : routers montés sous `/api`, `models.py`/ `importers.py`/`ingest.py` importés au démarrage pour `create_all` et les registres. 3. `models.py` : style SQLAlchemy 2.0 typé (`Mapped`/`mapped_column`), héritage `class X(TimestampMixin, Base)` ; noms de tables `snake_case` **pluriel** ; toute table métier porte `user_id = mapped_column(ForeignKey("users.id"), index=True)` ; datetimes en `DateTime(timezone=True)` et valeurs **UTC** ; colonnes « jour local » en `Date` ; poids kg, longueurs cm ou m (distances), volumes ml, énergie kcal, durées secondes, argent en **centimes** (`int`) ou `Numeric` pour les prix unitaires. 4. Table alimentée par import/ingestion → ajouter `SourceMixin` et les deux contraintes uniques : `(user_id, source, external_id)` et `(user_id, content_hash)` (noms `uq__external`, `uq_
_hash`). Agrégats journaliers → clé `(user_id, day, source)` + upsert `ON CONFLICT DO UPDATE`. 5. `schemas.py` : Pydantic v2 ; suffixes `XxxCreate`, `XxxUpdate` (champs optionnels), `XxxRead` (avec `model_config = ConfigDict(from_attributes=True)`) ; jamais de modèle ORM retourné sans schéma `Read`. 6. `service.py` : logique métier ; signature `def fn(db: Session, user_id: int, ...)`. Les routeurs restent minces (dépendances + appel service + schéma de réponse). Le service lève les sous-classes d'`AppError` (`NotFoundError`, `ConflictError`, `DomainValidationError`…) avec message **français** ; **interdit** de lever `HTTPException` dans un module. 7. Sécurité : tout endpoint (hors `auth` public et `healthz`) dépend de `get_current_user` et filtre **systématiquement** par `user.id`. Endpoints d'ingestion : dépendance `get_ingest_identity("ingest:")`. 8. Pagination : toute liste utilise `PageParams`/`Page[T]`/`paginate` de `core.pagination` — pas de pagination maison. Tri via `?sort=` (préfixe `-` = desc), champs autorisés explicites. 9. Requêtes : `select()` SQLAlchemy 2.0 uniquement (pas de `session.query`). Le routeur/service ne fait pas de `commit` partiel par ligne ; un `commit` par opération logique (le `get_db` fournit la session, le service commit). ### C3. Ajouter un importeur de fichier 1. Dans `importers.py` du module métier concerné, sous-classer `BaseImporter` (`app.core.importing.base`) et décorer avec `@register_importer`. 2. Renseigner `id` (snake_case unique, suffixe format : `_csv`, `_ofx`), `label` (français, affiché dans l'UI), `domain`, `accepted_extensions`. 3. `sniff()` ne lève jamais et reste bon marché (extension + en-têtes dans les 4096 premiers octets). `parse()` gère les encodages `utf-8-sig` puis `cp1252` et les CSV `;` à décimale virgule ; il produit des `NormalizedRecord` en **unités canoniques** avec `external_id` si la source en fournit un, sinon `dedupe_fields` pertinents (ex : `("posted_at", "amount_cents", "label_raw")`). 4. `upsert()` retourne `INSERTED` / `UPDATED` / `DUPLICATE` — la détection de doublon se fait par lookup sur les contraintes du C2.4 (pas par try/except d'IntegrityError en boucle). 5. Ne pas créer d'endpoint d'upload : `POST /api/imports` du module `imports` sert toutes les sources. ### C4. Ajouter un handler d'ingestion JSON Dans `ingest.py` du module : sous-classer `BaseIngestHandler`, décorer `@register_ingest_handler`, déclarer `domain` et `record_types`, implémenter `apply()` (mêmes règles de dédup que C3.4). Le endpoint `POST /api/ingest/{domain}` existe déjà ; il exige le scope `ingest:` (ou `ingest:*`) pour les clés d'appareil. ### C5. Créer un module frontend 1. Créer `apps/web/src/modules//` avec `index.ts` dont l'**export default** est un `ModuleManifest` (`src/types/module.ts`) : `id` = nom du dossier (anglais), `title` français, `order` réservé (home 0, health 10, vape 20, finance 30, imports 80, settings 90 ; nouveaux modules : dizaine libre), `routes` (chemins **absolus**, slugs **français** : `/sante/...`, `/vape/...`, `/finances/...`), `nav` (labels français + icône `lucide-react`). 2. **Ne jamais modifier** `router.tsx`, `modules.ts`, `Sidebar.tsx` : la découverte `import.meta.glob` est automatique. Ajouter une entrée de nav = ajouter un élément au tableau `nav` du manifeste de son module. 3. Fichiers standard du module : `api.ts` (hooks React Query + types TS des schémas API), `strings.ts` (chaînes françaises du module, export d'un objet constant — pas de français en dur éparpillé dans le JSX pour les libellés réutilisés), `pages/`, `components/`. 4. Données : **toujours** via les hooks React Query de `api.ts` du module, qui appellent le wrapper `api()` de `src/lib/api.ts` (jamais `fetch` direct). Clés de requête `[moduleId, resource, params]` ; toute mutation invalide `[moduleId, resource]`. 5. Graphiques : exclusivement via `` (`src/components/charts/EChart.tsx`) et le thème `lifetrack-dark` ; pas d'accès direct à `echarts.init` dans les pages ; formats d'axes/tooltips via `src/lib/format.ts`. 6. UI : composants partagés de `src/components/ui/` d'abord ; classes Tailwind (palette sombre du §7.5) ; pas de CSS externe, pas de CDN, pas de nouvelle dépendance sans l'ajouter à `package.json` du repo. 7. Pages : nom `XxxPage.tsx`, chargées via `React.lazy` dans le manifeste ; états vide/chargement/erreur systématiques (`EmptyState`, `Spinner`, message d'`ApiError.message`). ### C6. API — rappels contractuels - Préfixe `/api/` ; ressources au pluriel anglais ; `GET` liste paginée (`Page[T]`), `POST` création (`201`), `PATCH` partiel, `PUT` singleton, `DELETE` → `204`. - Forme d'erreur unique `{"error": {"code", "message", "details"}}` — `code` snake_case stable, `message` en français. - Datetimes : UTC ISO 8601 (`Z`) ; jours locaux : `YYYY-MM-DD` ; agrégations journalières : paramètre `?tz=` (défaut `Europe/Paris`). - Filtres temporels : `?from=`/`?to=` inclusifs. ### C7. Qualité - Python : `ruff` (lint + format), type hints partout, pas d'import inutilisé ; tests `pytest` dans `apps/api/app/tests/` (au minimum : contrat du routeur du module + dédup des importeurs). - TypeScript : `strict: true`, pas de `any` non justifié ; build `npm run build` sans erreur. - Aucune dépendance réseau à l'exécution côté web (fonts/icônes/librairies embarquées). - Secrets uniquement via variables d'environnement ; rien de sensible commité (`.env` est git-ignoré, `.env.example` documente). ### C8. Portabilité des types (tests SQLite) La suite pytest s'exécute sur **SQLite en mémoire** (PostgreSQL en production) : tous les modèles doivent donc utiliser des types de colonnes **portables**. 1. Énumérations : `sa.Enum(..., native_enum=False)` — jamais d'`ENUM` natif PostgreSQL. 2. UUID : `sa.Uuid` (type générique SQLAlchemy), pas `postgresql.UUID`. 3. JSON : `sa.JSON().with_variant(postgresql.JSONB, "postgresql")` — utiliser l'alias prêt à l'emploi **`JSONB_V`** exporté par `app/core/mixins.py` (JSON simple sous SQLite, JSONB sous PostgreSQL). 4. Index uniques partiels : déclarer **les deux** clauses `sqlite_where` **et** `postgresql_where` (mêmes expressions) ; **jamais** `postgresql_nulls_not_distinct`. 5. Rollback centralisé des imports : toute table portant une colonne référençant `import_runs.id` doit déclarer la clé étrangère avec **`ondelete="CASCADE"`** (`ForeignKey("import_runs.id", ondelete="CASCADE")`) — c'est ce qui permet à `DELETE /api/imports/{id}` de supprimer les lignes importées d'un lot. (Sous SQLite, le moteur active `PRAGMA foreign_keys=ON` automatiquement — voir `app/core/database.py`.)