# Journal de bord — SIOP V2

Trace chronologique des sessions (la plus récente en premier). Le **playbook** (`docs/0X-*`) est le livre ; ici, c'est le quotidien.

---

## 2026-08-02 — Pr. Daaif (+ Claude) — Recette R6 (Gestionnaire, partie 1) — 2 bugs corrigés

**Actions**

- Début de la recette complète promise en fin de R6.5 : parcours du référent en Gestionnaire
  (Nadia Berrada) sur iPhone physique, verrouillé côté serveur après chaque étape utile.
- **Bug réel 1 — Menu inaccessible en bas** : `app/(tabs)/menu.tsx` n'était PAS scrollable (un
  simple `View`, pas de `ScrollView`) — avec les 4 groupes maintenant pleinement câblés (12 liens
  et 4 en-têtes), le contenu dépasse la hauteur de l'écran et « Personnes & équipes » (dernier
  groupe) était strictement hors d'atteinte, sans aucun moyen d'y accéder. Présent depuis R6.1,
  seulement révélé une fois tous les groupes remplis. **Corrigé** : `ScrollView` autour de la
  liste des groupes, en-tête (EnteteTabs + titre) fixe au-dessus.
- **Bug réel 2 — BC créé sans confirmation visible** : `app/stock/[id]/commander.tsx` naviguait
  silencieusement en arrière après succès — le référent a créé DEUX bons de commande
  (BC-2026-1009, BC-2026-1010, vérifiés côté serveur, tous deux corrects) sans aucun retour dans
  l'app, doute légitime sur ce qui s'était vraiment passé. **Corrigé** : écran de confirmation
  explicite (référence, fournisseur, total, statut) avant de revenir, même patron que
  `personnes/lien.tsx`.
- **Amélioration Tiers** : le référent a signalé qu'on ne pouvait « ni ajouter ni consulter » —
  l'absence de fiche détail (délibérée, D4 — lecture seule) se lisait comme un écran cassé plutôt
  que simple. Arbitrage : fiche détail ajoutée (`app/tiers/[id].tsx` — identité, contact, BC en
  cours pour un fournisseur, sites rattachés pour un client, aucune nouvelle requête, tout déjà
  dans `usePartners()`), création/édition restent au web.
- **Faux positifs écartés, vérifiés côté serveur** :
  - Approbation de DEM-2026-0110 (« Voyant étage éteint ») : a fonctionné (statut `APPROVED`,
    OT-2026-1200 créé) — le message du référent citait juste le libellé de la demande.
  - Fichiers « Aucun document accessible » : confirmé — `GET /documents` renvoie `[]` sur cette
    instance. Pas un bug mobile : aucun document n'existe sur cette base locale (README/seed à
    vérifier séparément si un jeu de documents de démo est attendu).
  - Statistiques « les chiffres ne changent pas » entre 3/6/12 mois : confirmé côté serveur que la
    période EST bien transmise et traitée (`costsByMonth` varie en nombre de points), mais les
    indicateurs affichés (coût du mois, taux préventif, OT clôturés, pannes) ne varient pas parce
    que l'activité codée sur ce jeu de données est concentrée sur une fenêtre récente — réalité
    des données de démo, pas un bug du sélecteur.
- Vérifié après correctifs : typecheck propre, 17 tests Jest, lint 5/5 paquets.

**Décisions**

- Tiers gagne une fiche détail en lecture seule (voir ci-dessus) — seul écart au périmètre D4
  initial de R6.3, sur retour direct de recette.

**Prochaine étape** : suite de la recette Gestionnaire (Sites/Ascenseurs/Fichiers/Assistant+dictée
restent à confirmer), puis passage Demandeur (permissions les plus étroites).

---

## 2026-08-02 — Pr. Daaif (+ Claude) — R6.5 : Assistant mobile (chat sourcé + dictée)

**Actions**

- Maquette dédiée (`docs/02-design/maquettes/maquette-mobile-assistant.html`, 3 écrans) rédigée et
  validée par le référent : réponse sourcée, refus honnête chiffré, et — demande ajoutée en cours
  de revue — **dictée de la question**. Décisions actées : D1 un échange à la fois (pas
  d'historique multi-tours conservé, même limite que le web) ; D2 « Ouvrir l'OT » → fiche R4
  telle quelle, « Voir le document » → métadonnées seules (bibliothèque R6.3, pas d'ouverture de
  fichier tant qu'`expo-sharing` n'est pas ajouté) ; D3 refus chiffré + avertissement permanents,
  identiques au web ; D4 Assistant rejoint le groupe Pilotage pour tout rôle avec
  `WORK_ORDERS.view` ; **D5** la question peut être tapée OU dictée — même pipeline que la
  dictée déjà livrée en clôture (ADR-004 §5, faster-whisper local, audio jamais persisté) — la
  transcription REMPLIT le champ de saisie, éditable, jamais d'envoi automatique.
- `api/assistant.ts` (`useAskAssistant`, 503 géré comme le web — état attendu du contrat, pas une
  erreur).
- Écran **Assistant** (`app/assistant/index.tsx`) : chat à un échange à la fois, citations
  numérotées, cartes sources (extrait exact + « Voir le document »/« Ouvrir l'OT »), refus honnête
  chiffré avec « Reformuler », avertissement permanent. Micro dans la barre de saisie : réutilise
  telle quelle la mécanique de `cloture.tsx` (`useAudioRecorder`, `setAudioModeAsync`,
  `useTranscription`, purge `FileSystem.deleteAsync` quoi qu'il arrive) — aucun nouveau bug à
  découvrir, le pipeline est déjà recetté sur iPhone physique (01/08).
- Fiche document (`app/bibliotheque/[id].tsx`, nouveau) : métadonnées seules, destination de
  « Voir le document » — la liste `bibliotheque/index.tsx` (R6.3) y mène aussi désormais (les
  cartes étaient jusque-là décoratives, sans action).
- Menu : Assistant route réellement — **le Menu R6 n'a plus aucune entrée « à venir » dans les 4
  groupes** (Catégories excepté, admin, toujours hors périmètre mobile).
- Vérifié : typecheck propre, 17 tests Jest, lint 5/5 paquets. Contrat non touché.

**Décisions**

- Voir D1-D5 ci-dessus (maquette dédiée, seule sous-étape de R6 à avoir eu son propre tour de
  design-first plutôt qu'une exécution directe de la maquette R6 initiale).

**Prochaine étape** : **R6 fonctionnellement complet** (Exploitation/Parc/Ressources/Pilotage,
Assistant inclus, pour tous les rôles). Reste la recette complète avant de considérer R6 close au
sens R0→R5 (chaque sous-étape n'a été vérifiée que par typecheck/tests/lint jusqu'ici, jamais en
usage réel au-delà de la confirmation R6.1) ; `expo-sharing`/`expo-clipboard` en réserve pour une
prochaine étape native ; restes non bloquants inchangés (Dokploy, Android physique,
`AI_SEUIL_*`/darija, `DOKPLOY_WEBHOOK_URL`).

---

## 2026-08-02 — Pr. Daaif (+ Claude) — Confirmation référent : nav adaptative + déconnexion

**Actions**

- Le référent confirme sur son iPhone (app déjà connectée au serveur Metro) : la barre d'onglets
  adaptative (R6.1) et le correctif de déconnexion (`EnteteTabs`, tap simple + confirmation
  depuis n'importe quel onglet) fonctionnent tels que livrés. Dernier point resté ouvert depuis le
  début de R6 — clos.

**Décisions**

- Aucune.

**Prochaine étape** : une recette plus complète (parcourir Sites/Ascenseurs/Stock/Tiers/
Statistiques/Personnes sur au moins 2 rôles non-technicien) reste à faire avant de considérer R6
close au même sens que R0→R5 (chacune avait sa recette chiffrée avant tag) — pour l'instant
chaque sous-étape n'a été vérifiée que par typecheck/tests/lint, jamais en usage réel au-delà de
ce point précis. Sinon : maquette dédiée pour l'Assistant mobile, `expo-sharing`/`expo-clipboard`
en réserve.

---

## 2026-08-02 — Pr. Daaif (+ Claude) — R6.4 : Pilotage sur mobile (Statistiques, Personnes)

**Actions**

- `EXPO_PUBLIC_WEB_URL` (nouveau, `.env`) + `WEB_URL` (`api/client.ts`) : le lien d'activation
  « Personnes » pointe la page web `/activation?token=…` (il n'existe pas d'équivalent mobile,
  même page que R1) — variable par environnement comme `EXPO_PUBLIC_API_URL`.
- `apps/mobile/src/api/pilotage.ts` (nouveau) : `useAnalyticsSummary`, `useUsers`, `useRoles`,
  `useInviteUser`.
- Écran **Statistiques** (`app/statistiques/index.tsx`) : maquette écran 7 — sélecteur de période
  3/6/12 mois, coût du mois, taux préventif, OT clôturés, pannes par organe (top 5), top
  équipements en coût. Mêmes chiffres que `/analytics/summary` (web), en cartes plutôt qu'en
  graphes denses (D4).
- Écran **Personnes & équipes** (`app/personnes/index.tsx`) : liste avec statut (actif/invitation
  envoyée/désactivé) ; **Inviter** (`app/personnes/inviter.tsx` — nom, e-mail, rôle) ; **lien
  d'activation émis** (`app/personnes/lien.tsx`) affiché en texte **sélectionnable** (`Text
  selectable`, RN natif) plutôt que via presse-papiers — `expo-clipboard` est une dépendance
  native absente du projet, même raisonnement que `expo-sharing` en R6.3 : différée plutôt
  qu'ajoutée à la légère, un appui long suffit pour copier. Taux horaire, rôles, équipes restent
  gérés depuis le web (D4).
- Menu : Statistiques et Personnes & équipes routent réellement. **Assistant reste à venir** — un
  chat sourcé (citations, refus honnête) est un nouveau patron d'écran, jamais maquetté sur
  mobile (contrairement à Sites/Ascenseurs/Stock/Tiers/Personnes qui réutilisaient des patrons
  déjà validés Carte/LigneInfo/EnteteFiche) : mérite son propre tour de design-first plutôt que
  d'être exécuté par réflexe en fin de release.
- Vérifié : typecheck propre, 17 tests Jest, lint 5/5 paquets. Contrat non touché.

**Décisions**

- Aucune nouvelle sur le fond — exécution de la maquette R6 déjà validée. Deux mêmes arbitrages
  que R6.3 reconduits explicitement : pas de nouvelle dépendance native au milieu d'une passe
  (`expo-clipboard` comme `expo-sharing`), et l'Assistant IA mobile est repoussé à un maquettage
  dédié plutôt que forcé dans le périmètre « actions courantes » de R6.

**Prochaine étape** — **R6 quasi close** : Exploitation/Parc/Ressources/Pilotage (hors Assistant)
livrés pour tous les rôles. Restes : confirmation du référent sur iPhone (R6.1 + correctif
déconnexion, toujours en attente — condition de la clôture réelle de R6) ; maquette dédiée pour
l'Assistant mobile (question ouverte : l'ajouter à R6 ou une release à part) ; `expo-sharing` et
`expo-clipboard` pour les flux mobiles restés en lecture seule ; restes non bloquants inchangés
(Dokploy, Android physique, `AI_SEUIL_*`/darija, `DOKPLOY_WEBHOOK_URL`).

---

## 2026-08-02 — Pr. Daaif (+ Claude) — R6.3 : Ressources sur mobile (Stock, Tiers, Fichiers)

**Actions**

- `apps/mobile/src/api/ressources.ts` (nouveau fichier, mêmes opérations que `gestion.ts` côté
  web) : `usePartners`, `useParts`/`usePart`, `useCreatePurchaseOrder`, `useDocuments` (bibliothèque
  complète, sans filtre — distincte de `useDocumentsOT` déjà scopée à un OT).
- Écran **Stock** (`app/stock/index.tsx`) : pièces sous seuil en tête (même logique que `stock.tsx`
  web), badge compteur. **Fiche pièce** (`app/stock/[id].tsx`) : identité + « Commander » si
  `PURCHASE_ORDERS.create` ET fournisseur associé — sinon message explicite plutôt qu'un bouton
  mort. **Nouveau BC** (`app/stock/[id]/commander.tsx`) : une seule ligne pré-remplie depuis
  l'alerte (fournisseur figé, quantité par défaut = manquant jusqu'au seuil, prix par défaut =
  dernier prix connu) — le bon de commande multi-lignes détaillé reste au web (D4).
- Écran **Tiers** (`app/tiers/index.tsx`) : liste fournisseurs/clients, **lecture seule** sur
  mobile pour cette passe (D4 — création/édition réservées au web).
- Écran **Fichiers** (`app/bibliotheque/index.tsx`) : métadonnées seulement (nom, taille,
  rattachement), **pas d'ouverture/téléchargement** — ce flux demande `expo-sharing` (dépendance
  native absente du projet) et donc un nouveau cycle de build natif ; reporté plutôt
  qu'ajouté à la légère au milieu de cette passe. Le bandeau de l'écran le dit explicitement.
- Menu : Stock & achats / Tiers / Fichiers routent réellement.
- Vérifié : typecheck propre, 17 tests Jest, lint 5/5 paquets. Contrat (`schema.d.ts`) non touché
  — toutes les opérations utilisées existaient déjà (R3).

**Décisions**

- Aucune nouvelle — Ressources exécute la maquette R6 déjà validée (écran 6, D4) ; Tiers/Fichiers
  n'y étaient pas maquettés en détail mais suivent la même logique de consultation déjà actée,
  avec les patrons visuels déjà validés (Carte/LigneInfo/EnteteFiche).
- L'ouverture de document sur mobile est explicitement différée à une prochaine étape (nouvelle
  dépendance `expo-sharing` = nouveau build natif à valider, pas à empiler sur cette passe).

**Prochaine étape** : R6.4 (Pilotage — Statistiques, Personnes, Assistant) ; confirmation du
référent sur iPhone pour R6.1/logout, toujours en attente.

---

## 2026-08-02 — Pr. Daaif (+ Claude) — R6.2 : Parc sur mobile (Sites, Ascenseurs)

**Actions**

- `useLocations()` mobile (`GET /locations`, même liste plate que le web — le client construit
  l'arbre site → zone).
- Écran **Sites** (`app/sites/index.tsx`) : sites de premier niveau (`parentId === null`), nom,
  ville/adresse, nombre d'appareils.
- **Fiche site** (`app/sites/[id].tsx`) : identité (adresse, gardien, client/syndic), zones avec
  leurs appareils, liste des ascenseurs du site — même filtre `siteName` que `fiche-site.tsx`
  (web). Pas de carte interactive ni d'édition sur mobile (D4 — consultation seule pour cette
  famille, réservé au web pour l'instant).
- Écran **Ascenseurs** (`app/ascenseurs/index.tsx`) : le parc complet, déjà préchargé (D1 lecture
  locale, même donnée que le Scanner), trié par référence. Ouvre la fiche appareil R4 telle
  quelle (`ascenseur/[id].tsx`, déjà générique — aucune modification nécessaire, elle dérive déjà
  ce qu'elle affiche du rôle appelant côté serveur).
- Menu : « Ascenseurs » et « Sites » routent maintenant vers ces écrans réels (retirés de la
  liste « à venir ») ; « Catégories » reste à venir (admin, hors périmètre R6.2).
- Vérifié : typecheck propre, 17 tests Jest, lint 5/5 paquets.

**Décisions**

- Aucune nouvelle — exécution directe de la maquette R6 déjà validée (Parc = D4, consultation
  seule) ; pas de nouveau tour de maquette pour cette sous-étape, les patrons visuels réutilisés
  (Carte/LigneInfo/EnteteFiche) sont déjà validés depuis R4.

**Prochaine étape** : R6.3 (Ressources — Stock, Tiers, Fichiers) ; confirmation du référent sur
iPhone pour R6.1 et le correctif déconnexion, toujours en attente.

---

## 2026-08-02 — Pr. Daaif (+ Claude) — R6 lancée : mobile ouvert à tous les rôles

**Contexte** — deux retours du référent en test réel sur iPhone : (1) une fois authentifié,
impossible de revenir en arrière ou de se déconnecter ; (2) l'app mobile devrait être accessible
à tous les rôles, pas seulement au Technicien, avec les mêmes actions que le web. Le premier est
un bug, corrigé dans la foulée ; le second est un changement de fond — R4 avait délibérément
fermé le périmètre au Technicien (décision actée dans ses maquettes). Ouvrir à tous les rôles
veut dire porter sur mobile une bonne partie de ce que fait le web (référentiel, ressources,
personnes, statistiques…) : plus gros que R4 dans son ensemble, donc une **nouvelle release,
R6** (R4 est close depuis le 17/07, R4.1/R4.2/R4.3 déjà pris par le socle mobile technicien
d'origine — pas question de les réutiliser et de brouiller l'historique).

**Actions**

- **Correctif déconnexion** : `EnteteTabs` (`composants/ui.tsx`) factorise une entête commune aux
  4 onglets terrain — badge de compte + confirmation `Alert` au tap simple (l'ancien geste,
  `onLongPress` sur « Ma journée » seulement, n'était pas découvrable). Le rôle affiché vient du
  compte connecté, plus de libellé « Technicien » figé.
- **Maquette R6** (`docs/02-design/maquettes/maquette-mobile-tous-roles.html`, 7 écrans) rédigée
  et validée par le référent le jour même : synthèse par rôle, Accueil adaptatif, Menu groupé
  filtré par la matrice, OT côté Dispatcher, Demandes (création + approbation), Stock, Personnes
  & statistiques. Cinq décisions actées : D1 barre d'onglets adaptative (Technicien/Technicien
  limité inchangés + onglet Menu ajouté) ; D2 le Menu reprend à l'identique les 4 groupes du web
  (Exploitation/Parc/Ressources/Pilotage), même matrice, aucune règle de droit nouvelle ; D3 un
  groupe sans lien visible est masqué en entier (contrairement au web) ; D4 le mobile porte les
  actions courantes par famille, pas les flux de gestion les plus denses (réservés au web pour
  l'instant) ; D5 aucune logique de permission propre au mobile — un hook `usePermissions()`
  calqué sur celui du web.
- **R6.1 — socle** : `usePermissions()` mobile (lit `me.permissions`, déjà renvoyé par
  `/users/me`) ; barre d'onglets adaptative (`ongletsVisibles` par rôle, `href: null` pour masquer
  sans retirer du navigateur — le Menu peut toujours y pousser) ; écrans Accueil (dashboard réel
  pour Administrateur/Gestionnaire/Dispatcher/Vue seule — aucun chiffre inventé, seuls
  WORK_ORDERS/REQUESTS sont câblés), OT (liste complète, `viewOther` déjà géré serveur, réutilise
  la fiche OT R4 telle quelle — elle dérive déjà ses actions de `allowedTransitions`), Menu
  (groupes filtrés, groupe vide masqué), Demandes (`PanneauDemandes`, un seul composant pour tous
  les rôles : création + suivi pour le Demandeur, approbation/rejet à motif pour
  Gestionnaire/Dispatcher/Administrateur, lecture seule pour Vue seule — même patron que le web,
  découverte au passage : `GET /assets/options` déjà ouvert à tout rôle authentifié, réutilisé
  tel quel). Familles pas encore portées (Sites, Ascenseurs, Stock, Tiers, Fichiers, Statistiques,
  Assistant, Personnes) : écran « à venir » honnête (`composants/a-venir.tsx`, déjà présent mais
  jamais câblé jusqu'ici) plutôt qu'un lien mort — chacune mérite son propre passage, sur le
  modèle R4.1→R4.3.
- Vérifié : typecheck propre, 17 tests Jest verts, lint 5/5 paquets. Pas de vérification visuelle
  en navigateur de mon côté (aucun outil de ce type dans cet environnement) — à confirmer par le
  référent sur l'iPhone déjà connecté au serveur Metro.

**Décisions**

- Nouvelle release **R6** (pas R4.1/4.2/4.3, déjà pris ; pas de réouverture de R4, clos) pour tout
  le chantier « mobile ouvert à tous les rôles ». Sous-étapes à venir : R6.2 (Parc — Sites,
  Ascenseurs), R6.3 (Ressources — Stock, Tiers, Fichiers), R6.4 (Pilotage — Statistiques,
  Personnes, Assistant), sur le modèle des sous-releases R4.1→R4.3.

**Prochaine étape** : R6.2 (Parc sur mobile) ; confirmation du référent sur iPhone (nav adaptative
et correctif déconnexion) ; restes non bloquants inchangés (recette Android physique,
redéploiement Dokploy, calibrage `AI_SEUIL_*`/darija, `DOKPLOY_WEBHOOK_URL`).

---

## 2026-08-01 — Pr. Daaif (+ Claude) — Dictée validée sur iPhone physique (ADR-004 §5)

**Actions**

- Reprise du reste laissé le 22/07 : le test tactile de la dictée sur iPhone, alors bloqué par une connexion
  USB qui ne se rétablissait pas. Diagnostic cette fois : `system_profiler` (l'outil utilisé pour vérifier la
  détection USB) était lui-même en panne et renvoyait un faux négatif silencieux — `ioreg` a révélé que
  l'iPhone était en réalité détecté. Le vrai câble/port n'a jamais été le problème.
- Une fois l'app reconstruite (certificat gratuit expiré après plus d'une semaine — régénéré via le pipeline
  officiel `expo run:ios`, jamais par un contournement manuel), **deux bugs réels trouvés et corrigés** :
  1. `expo-audio` sur iOS refuse d'enregistrer sans un appel explicite à `setAudioModeAsync({ allowsRecording:
     true })` avant `recorder.record()` — absent du code initial, jamais détecté hors d'un vrai appareil.
  2. Le correctif des modules Expo/RN précompilés (ADR-005, `EXPO_USE_PRECOMPILED_MODULES=0` /
     `RCT_USE_PREBUILT_RNCORE=0`) n'avait **jamais été committé** dans `apps/mobile/.env`, contrairement au
     correctif FormData voisin déjà permanent — un vrai trou qui aurait refait planter l'app à la prochaine
     reconstruction depuis zéro. Corrigé : les deux variables vivent maintenant côte à côte dans `.env`.
- **Recette réelle bout en bout** : micro → dictée d'une phrase → transcription fidèle (`faster-whisper` réel)
  → relecture → sauvegarde dans le bilan → clôture de l'OT → réindexation du corpus → **le contenu dicté
  retrouvé par la recherche sémantique** (score 0,476). La boucle complète de l'idée d'origine (« cette
  transcription devrait alimenter le corpus ») est validée sur vrai matériel, vraie voix, vrai réseau.
- Infra locale relancée depuis zéro après une semaine d'inactivité (Docker, conteneurs `siop2-*`, API,
  service IA) — occasion de vérifier que le redémarrage à froid fonctionne proprement.

**Décisions**

- Aucune — cette session referme un reste déjà décidé, sans nouvelle décision de conception.

**Prochaine étape** : recette Android sur appareil physique (toujours en attente d'un appareil) ; redéploiement
Dokploy (`AI_SERVICE_TOKEN`) ; calibrage `AI_SEUIL_*` et qualité darija sur corpus SPELEV réel ; secret
`DOKPLOY_WEBHOOK_URL`.

---

## 2026-07-22 — Pr. Daaif (+ Claude) — Dictée implémentée : audio → transcription locale → corpus (ADR-004 §5)

**Actions**

- Suite du feu vert « on passe à l'audio » (maquette Voix amendée le 22/07) : implémentation complète de la
  dictée (R5 D5) — écran maquetté jamais construit jusqu'ici, ni open source ni local en LLM externe, choix du
  référent réaffirmé (« toujours pour l'open source et le local »).
- **`apps/ai`** : `faster-whisper` (CTranslate2, CPU, MIT), `AI_TRANSCRIPTION=off|locale|deterministe` (défaut
  off), `AI_TRANSCRIPTION_MODEL` (défaut `small`) ; endpoint `/internal/transcrire` — l'audio ne survit
  JAMAIS à l'appel (fichier temporaire supprimé quoi qu'il arrive, succès ou erreur) ; `indexer_bilans` inclut
  désormais `InterventionReport.note` (anonymisée par le même pipeline D4) — le champ existait en base et au
  contrat depuis R2 mais **n'avait jamais eu d'écran** ; 29 pytest (dont le transcripteur déterministe pour CI).
- **Contrat** (77 opérations) : `POST /assistant/transcribe` (multipart, `TranscriptionResult`).
- **API NestJS** : proxy multipart vers `siop2-ai` (`AssistantService.transcribe`), `WORK_ORDERS.edit` — même
  droit que la saisie du bilan qu'elle alimente ; 2 tests e2e ajoutés (80 tests API au total).
- **Mobile** : `expo-audio` (enregistrement) + `expo-file-system` (purge locale) ; carte « Décrire pour
  suggérer » de l'écran de clôture gagne un bouton dicter/terminer, une confirmation de purge, et
  « Joindre la description à l'OT » (sauvegarde dans `note` via la file existante — corrige au passage un bug
  latent : `enfilerBilan` ignorait silencieusement toute mise à jour de `note`).
- **Docker** : `siop2-ai` embarque désormais aussi le modèle Whisper au build (image 1,54 Go → 2,19 Go) —
  construit et vérifié réellement (transcription en conteneur, non-root, 0 téléchargement au démarrage).
- **Vérification réelle** (voix de synthèse macOS `say`, français) : transcription fidèle en direct
  (`TranscripteurLocal`), bout en bout via l'API NestJS, et dans le conteneur Docker construit — puis chaîne
  complète corpus confirmée : note sauvegardée → OT clôturé → réindexation → contenu retrouvé par recherche
  sémantique avec un bon score de pertinence.
- **Nettoyage** : la base de dev locale, polluée par les tests manuels de la recette terrain (deux OT seedés
  clôturés en dehors de leur état d'origine), a été réinitialisée avec l'accord explicite du référent
  (`prisma migrate reset --force`, bloqué par défaut pour un agent IA — consentement demandé et obtenu avant
  exécution).

**Décisions**

- Pas de prototype de mesure français/darija avant l'implémentation (confirmé une seconde fois) — seuls des
  contrôles d'ingénierie de base (le code tourne, avec de la vraie parole) ont été faits, pas un calibrage.
- **Reste** : le test tactile sur iPhone physique (bouton dicter, permission micro) n'a pas pu se jouer —
  blocage USB persistant malgré câble/port/redémarrage/mode développeur essayés à plusieurs reprises. Reporté
  comme la recette Android, sur décision du référent — ne bloque pas la suite.

**Prochaine étape** : test tactile de la dictée sur iPhone dès que la connexion USB fonctionnera ; recette
Android sur appareil physique ; redéploiement Dokploy (`AI_SERVICE_TOKEN`) ; calibrage `AI_SEUIL_*` et qualité
darija sur corpus SPELEV réel.

---

## 2026-07-22 — Pr. Daaif (+ Claude) — Idée backlog : transcription audio → corpus (maquette amendée, pas codée)

**Actions**

- Discussion de conception (aucun code) : étendre l'écran « Voix » de R5 (dictée, jamais construite — option
  « si budget temps ») pour que la transcription relue, une fois jointe à l'OT, **alimente aussi le corpus** de
  l'assistant à la clôture — pas seulement la description libre ponctuelle de la suggestion.
- Écarté : Ollama local pour les embeddings du corpus (déjà 100 % locaux via `fastembed`, aucun gain — pertinent
  plutôt pour remplacer l'API Claude opt-in de génération, piste non creusée).
- Moteur retenu pour une future transcription locale : `faster-whisper` (CTranslate2, MIT, CPU) — même
  philosophie que les embeddings. Point de vigilance identifié : Whisper transcrit mal le darija, probable en
  mélange avec le français sur le terrain.
- Point d'accroche trouvé dans le schéma existant : `InterventionReport.note` (texte libre) existe déjà en base
  et en UI mais n'est **pas encore indexé** dans le corpus (`indexer_bilans` ne reprend que les libellés codés).
- Gap identifié dans `anonymisation.py` : reconnaît seulement les noms CONNUS de la base, pas de NER générale —
  risque de fuite plus élevé sur de la parole libre que sur des libellés codés.
- **Maquette amendée et validée** (`maquette-r5.html`, écran 6 « Voix ») : bandeau d'intention et carte
  « Et ensuite » disent maintenant explicitement que le texte (anonymisé) rejoint le corpus à la clôture de l'OT.

**Décisions**

- Pas de prototype de mesure préalable (contrairement au calibrage des embeddings R5) — le calibrage
  français/darija se fera plus tard si besoin.
- **Implémentation non demandée pour l'instant** — la maquette est validée, le code attend un prochain feu vert.

**Prochaine étape** : reprendre sur demande du référent — implémentation cohérente avec le flux esquissé (audio
jamais persisté, transcription → `InterventionReport.note` → corpus à la clôture), moteur et calibrage déjà
tranchés, pas à rediscuter.

---

## 2026-07-19 — Pr. Daaif (+ Claude) — Recette terrain mobile sur iPhone 15 Pro : les 7 points validés (ADR-005)

**Actions**

- **La recette « mode avion » due depuis `release/r4`** a enfin pu se jouer sur un vrai appareil (iPhone 15 Pro
  du référent). Premier obstacle immédiat : Expo Go (App Store, dernière version) refuse le projet — « supported
  SDK 54 » contre notre SDK 57, retard d'approbation Apple structurel, hors de notre contrôle. **Décision du
  référent** : construire des builds natifs locaux (Xcode/Android Studio, signature gratuite via Apple ID
  personnel) pour la vraie recette terrain, tout en conservant Expo Go comme canal léger pour un aperçu sans
  installation ailleurs — actée dans **ADR-005**.
- **Cinq obstacles techniques réels** rencontrés et corrigés en route (détaillés dans ADR-005) : ciblage par UDID
  plutôt que nom d'appareil (apostrophe) ; ne jamais contourner `expo run:ios` par un `xcodebuild` manuel (a
  produit un crash `dyld: Library not loaded React.framework`) ; **incompatibilité des modules Expo/RN
  précompilés (SDK 56/57) avec la liaison statique du projet** — fix `EXPO_USE_PRECOMPILED_MODULES=0` +
  `RCT_USE_PREBUILT_RNCORE=0` ; **`fetch` global (`expo/fetch`) incompatible avec l'upload multipart natif**
  (`Unsupported FormDataPart implementation`, classé à tort « réseau indisponible » par notre propre gestion
  d'erreur) — fix permanent `EXPO_PUBLIC_USE_RN_FETCH=1` committé dans `apps/mobile/.env` ; découverte réseau du
  dev client peu fiable sur ce Wi-Fi (IP du Mac changée 4 fois, reprises via `expo run:ios --device <udid>` qui
  transmet l'adresse de Metro par deep link plutôt que par découverte automatique).
- **Recette terrain iOS — 7/7 points validés sur iPhone physique** : (1) connexion démo ; (2) scan caméra réel —
  résolution locale d'une étiquette réelle (D4) + rejet propre d'un QR étranger ; (3) vrai mode avion (D1) — file
  d'écriture visible ; (4) verrou optimiste (D2) — conflit provoqué en modifiant l'OT côté serveur pendant la
  coupure, écran Synchro affichant le conflit, tranché par l'humain, OT clos avec bilan complet et **photo
  réellement téléversée** (885 Ko, compression D5 confirmée) ; (5) persistance — saisie en file survivant à une
  fermeture complète + reconstruction de l'app, resynchronisée au retour réseau (limite documentée : un build dev
  client ne peut PAS démarrer à froid hors-ligne, aucun bundle embarqué — un vrai test « cold start offline »
  demanderait un build Release, hors périmètre aujourd'hui) ; (6) suggestions IA R5 au vrai modèle sur le
  téléphone, chips appliqués pré-remplissant les sélecteurs ; (7) sécurité ADR-003 — déconnexion (appui long,
  purement locale) puis bascule de compte démo, file et cache vides, aucune fuite entre comptes.
- **Android** : build natif installé sur l'émulateur (`Pixel_3a_API_34`, Android Studio déjà en place) ; passage
  santé complet — connexion démo, navigation Ma journée → fiche appareil (résolution de référence manuelle,
  équivalent du scan sans caméra d'émulateur, confirmée fonctionnelle), écran Scanner sans plantage.

**Décisions**

- ADR-005 actée : deux canaux de distribution mobile (builds natifs pour la vraie recette, Expo Go pour l'aperçu
  sans installation), managed workflow conservé (`ios/`/`android/` jamais committés).
- La validation terrain due depuis `release/r4` est **levée pour iOS**. La recette sur **appareil Android
  physique** (vrai mode avion, vraie caméra) reste un reste, faute d'appareil disponible aujourd'hui — même
  situation qu'iOS avant cette session.

**Prochaine étape** : recette Android sur appareil physique quand disponible ; redéploiement Dokploy
(`release/r3` puis r5 avec `AI_SERVICE_TOKEN`) ; calibrage `AI_SEUIL_*` sur corpus SPELEV réel ; secret
`DOKPLOY_WEBHOOK_URL`.

---

## 2026-07-17 — Pr. Daaif (+ Claude) — 🏁 R5 CLOSE : tag `release/r5`

**Actions**

- Tag annoté `release/r5` posé sur `cccfaaa` (CI verte, 8/8 jobs) et poussé — sur décision du référent,
  après recette sans clé API et revue pixel validée (arbitrage tableau appliqué).
- R0 → R5 : le périmètre v1 du plan de releases est couvert. La suite est de l'exploitation :
  déploiements, calibrage, recettes différées.

**Décisions**

- R5 est la dernière release du plan — les évolutions suivantes (voix opt-in « si budget temps »,
  backlog v2) s'ouvriront par de nouvelles maquettes, même méthode.

**Prochaine étape** : redéploiement Dokploy de l'instance ENSET (main = release/r5 ; nouvelles variables
`AI_SERVICE_TOKEN` obligatoire — runbook §2 — puis « Réindexer tout »), recette R4 sur téléphone (Expo Go,
due avant production client mobile), calibrage `AI_SEUIL_*` sur corpus SPELEV réel, secret `DOKPLOY_WEBHOOK_URL`.

---

## 2026-07-17 — Pr. Daaif (+ Claude) — Arbitrages de recette R5 rendus : le corpus passe en tableau

**Actions**

- **Arbitrage du référent** sur la revue pixel : « Ça serait mieux d'utiliser un tableau car la plupart des
  fichiers vont être des PDF » — le reste validé (« Appliquer » à écriture directe, calibrage au runbook).
- **Bibliothèque en tableau**, conforme à l'écran 5 de maquette-r5 : colonnes Document (nom + type · taille ·
  date · auteur), Rattaché à, Indexation, Corpus (interrupteur, visible pour ASSETS.edit), actions
  Ouvrir/Télécharger/Supprimer. Les vignettes restent aux cartes « Documents » des fiches (photos d'OT).
- Détail aligné sur la maquette : une image non indexable affiche son interrupteur **éteint** quel que soit
  l'état stocké — l'interrupteur montre la réalité du corpus, pas une colonne de base.
- e2e adapté (lignes de tableau), **16/16 Playwright rejoués** ; artefact de revue pixel mis à jour (même URL),
  arbitrages marqués rendus.

**Décisions**

- Revue pixel R5 **validée par le référent** (1 correction appliquée). Le tag `release/r5` peut être posé.

**Prochaine étape** : tag `release/r5` sur le mot du référent. Restes : recette R4 sur téléphone (Expo Go),
redéploiement Dokploy (`release/r3` puis r5 avec `AI_SERVICE_TOKEN`), calibrage `AI_SEUIL_*` sur corpus SPELEV.

---

## 2026-07-17 — Pr. Daaif (+ Claude) — Recette R5 (sans clé API) + durcissement production `siop2-ai`

**Actions**

- **Recette rejouée au vrai modèle, mode extractif (aucune clé API)** sur un corpus mis en scène (notice
  Otis Gen2 générée, 2 pages). Elle a **invalidé le modèle d'embeddings choisi en R5.1** : sur la question
  type du plan (« quel couple de serrage pour les guides du Gen2 ? »), MiniLM-384 classait la page-réponse
  DERRIÈRE des passages sans rapport (0,24 contre 0,41). Banc comparatif mesuré : `mpnet-base-v2` multilingue
  (768 d) rétablit le classement et une marge signal/bruit exploitable (pertinent ≥ 0,46, hors-corpus ≤ 0,42) ;
  `multilingual-e5-large` (1 024 d, 2,2 Go) classait bien aussi mais scores compressés (0,73-0,90) et poids
  rédhibitoires. **Bascule vers mpnet** (ADR-004 amendé, migration `r5_embeddings_mpnet` : pgvector 384 → 768,
  index vidé — re-dérivable par « Réindexer tout ») + **découpage affiné** (~350 caractères : la phrase-réponse
  ne se noie plus, l'extrait cité est lisible) + seuils par défaut recalés (0,45 / 0,40 / 0,55).
- **Recette validée après bascule** : réponse sourcée p. 2 en tête ✓, refus honnête chiffré sur question hors
  corpus ✓ (« routeur wifi » → refus ; charabia → refus), suggestions étagées ✓, D1-D5 tenues, le tout SANS
  clé. 16/16 Playwright (l'embeddeur déterministe suit les 768 dims sans recalibrage), 78 tests API, 23 pytest.
- **Revue pixel** : captures réelles des 6 écrans (web ×4, liseré « suggéré », mobile) face aux écrans de
  maquette-r5 — artefact publié pour le référent avec 3 points d'arbitrage (« Appliquer » à écriture directe,
  vignettes vs tableau du corpus, refus « voisin de domaine » dépendant du calibrage client).
- **Durcissement production** : `apps/ai/Dockerfile` (uv, venv non éditable, **modèle ONNX téléchargé au
  build** — ADR-004 §1, non-root, healthcheck) ; compose Dokploy : service `siop2-ai` interne (jamais sur
  `dokploy-network`, secret `AI_SERVICE_TOKEN` requis, génération opt-in par variables, seuils calibrables),
  `siop2-api` branché (`AI_SERVICE_URL`) ; runbook enrichi (§2 variables, §5 service IA, §6 calibrage des
  seuils sur corpus client, réindexation post-déploiement).

**Décisions**

- Le refus « voisin de domaine » (question ascenseur absente du corpus) reste dépendant du calibrage : jamais
  d'invention (extraits réels cités), mais pas toujours un refus. Les seuils sont des variables d'environnement
  pour être calibrés sur le corpus SPELEV réel — procédure au runbook §6.
- Le tag `release/r5` attend la validation de la revue pixel par le référent.

**Prochaine étape** : validation du référent (revue pixel + arbitrages) → tag `release/r5`. Restes : recette
R4 sur téléphone (Expo Go), redéploiement Dokploy (`release/r3` puis r5), secret `DOKPLOY_WEBHOOK_URL`.

---

## 2026-07-17 — Pr. Daaif (+ Claude) — R5.3 : les écrans de l'IA (web + mobile) et le corpus administrable

**Actions**

- **Contrat** (76 opérations) : `Document` expose son état de corpus (`inCorpus`, `indexedAt`, `chunkCount`), `PATCH /documents/{id}/corpus` (bascule réversible, ASSETS.edit), `POST /assistant/reindex` (bilan chiffré traduit du dialecte interne). Clients web **et mobile** régénérés — le job ci-contract vérifie désormais les deux.
- **Web — page Assistant** (sidebar Pilotage, WORK_ORDERS.view) : chat fidèle aux écrans 1-2 de maquette-r5 — réponse avec **extraits exacts cités** (« Ouvrir » → PDF en onglet authentifié, OT → fiche), avertissement « l'IA propose, vous validez » permanent ; **refus honnête chiffré** (« j'ai cherché dans N documents et M bilans ») avec l'action utile (téléverser la notice).
- **Web — Bibliothèque = corpus** (écran 5) : bandeau 09-08 (l'anonymisation est DITE), statut par document (indexé · date · extraits / à indexer / exclu / image non indexable), interrupteur d'exclusion (PDF seulement), « Réindexer tout » avec bilan. **Fiche OT** (écran 3) : « Décrire pour suggérer » dans la carte Bilan — suggestions justifiées (« N bilans similaires », confiance), « Appliquer » = le geste humain qui écrit (cohérent avec la carte à enregistrement direct), **liseré « suggéré »** retiré dès qu'un choix manuel reprend la main.
- **Mobile — clôture** (écran 4) : décrire au pouce → **chips suggérées** (un appui = un champ pré-rempli, ✓), note D1 visible ; hors-ligne le bouton dit « réseau requis » (l'IA est un service serveur — la clôture en file R4 n'en dépend pas).
- **`apps/ai`** : seuils par env (`AI_SEUIL_*`) — nécessaires à la CI et au calibrage de recette.
- **CI** : le job e2e démarre `siop2-ai` (embeddeur déterministe) et le parcours R5 se recette en vrai : upload d'un **PDF généré avec xref valide** → réindexation → statut → réponse sourcée (extrait « 25 Nm », p. 1) → refus sur charabia → exclusion → suggestion appliquée sur un OT créé. **16/16 Playwright** (recettes R0→R5).
- **Vérifications réelles** : seuils CI **mesurés** (vrai match 0,66 vs bruit de collisions 0,11 → pertinence 0,20 ; codes pertinents 0,14-0,16 vs parasite 0,08 → suggestion 0,10) ; chaîne complète au **vrai modèle ONNX** (réindexation, ask sourcé p. 8/p. 10, suggestions étagées avec vrais comptes de bilans) ; parcours **mobile Expo web 6/6** et **web 7/7** au vrai modèle, 0 erreur console. 78 tests API (2 nouveaux : bascule corpus gardée, reindex traduit + 403).

**Décisions**

- Sur le web, « Appliquer » écrit immédiatement (comme tout le reste de la carte Bilan R2) : le clic EST la validation humaine — l'esprit de la maquette (« rien sans vous ») est porté par le geste, pas par un bouton « Enregistrer » séparé. À montrer en revue pixel.
- Les seuils par défaut du vrai modèle (0,30/0,35/0,55) restent **à calibrer en recette sur le corpus client réel** : mesuré ce jour sur un corpus non-métier, le multilingual-MiniLM donne des scores plats (hors-sujet 0,35-0,43 vs pertinent 0,24-0,28) — c'est exactement pourquoi ils sont configurables.

**Prochaine étape** : recette R5 complète (doit passer SANS clé API), durcissement production (Dockerfile `siop2-ai` + compose Dokploy), puis tag `release/r5`. Restes : recette R4 sur appareil, redéploiement Dokploy de `release/r3`.

---

## 2026-07-17 — Pr. Daaif (+ Claude) — R5.2 : l'assistant au contrat + suggestion de codes de bilan

**Actions**

- **`apps/ai`** : `/internal/ask` — recherche → **seuil de pertinence** → extraits sourcés, ou **refus honnête** portant la taille du corpus cherché (« 1 document, 3 bilans » — l'écran 2 des maquettes aura ses chiffres) ; la rédaction passe par le `Generateur` (opt-in — mode extractif par défaut). `/internal/suggest` — similarité sémantique entre la description libre et les libellés **actifs** des référentiels (contextualisés « anomalie constatée : … »), un seul code par champ, au-dessus du seuil, confiance FORTE/MOYENNE + « N bilans similaires sur ce parc » (comptés dans les chunks d'historique). Sans LLM : déterministe, explicable. 23 pytest.
- **Contrat** (74 opérations) : `POST /assistant/ask` → `AssistantAnswer` (mode EXTRACTIVE/GENERATED/REFUSAL, extraits cités document/page ou bilan daté, corpus cherché) ; `POST /assistant/suggest-bilan` → codes existants seulement. Clients web/mobile régénérés.
- **API NestJS** : module `assistant` — proxy vers `siop2-ai` (`AI_SERVICE_URL`/`AI_SERVICE_TOKEN` à l'env, ADR-004 §4), permissions par la matrice (`ask` = WORK_ORDERS.view ; `suggest` = WORK_ORDERS.edit — qui remplit des bilans), traduction du dialecte interne vers le contrat, **503 propre** si le service IA est éteint (jamais un 500). 6 tests e2e sur un **stub HTTP** (76 tests API, 14 suites).
- **Vérifié sur la vraie chaîne** (NestJS → ai → pgvector, vrai modèle) — et elle a débusqué un bug : fastembed ne renvoie pas des vecteurs normés, la similarité des suggestions dépassait 1 (pgvector normalisait dans son opérateur, ce qui masquait l'écart). **Normalisation à l'encodage** + réindexation : bilans du parc trouvés en tête (0.41), refus honnête hors corpus, suggestions cosinus ≤ 1 à confiances étagées.

**Décisions**

- Les seuils (pertinence 0.30, suggestion 0.35, confiance forte 0.55) sont des constantes de départ — à calibrer en recette sur le corpus réel du client.

**Prochaine étape** : R5.3 — les écrans validés (Assistant sidebar, corpus dans la Bibliothèque, suggestions dans les fiches OT web et clôture mobile). Restes : recette R4 sur appareil, redéploiement Dokploy de `release/r3`.

---

## 2026-07-17 — Pr. Daaif (+ Claude) — R5.1+ : clé API de génération configurable (demande du référent)

**Actions**

- **`generation.py`** : l'interface `Generateur` d'ADR-004 §3 prend corps — `GenerateurExtractif` (contrat de base, aucun LLM) et `GenerateurAPI` (SDK officiel `anthropic`, dépendance **optionnelle** `--extra generation`, absente des tests/CI). Consigne système : citations [n] obligatoires depuis les extraits fournis, « n'invente RIEN », rappel de validation humaine. Tout échec (refus du modèle, quota, réseau) **retombe sur l'extractif** — jamais d'erreur utilisateur imputable au LLM ; `stop_reason == "refusal"` traité explicitement.
- **Configuration complète et validée au boot** : `AI_GENERATION=off|api`, `AI_API_KEY` (exigée en mode api — le démarrage refuse sinon, message clair), `AI_MODEL` (défaut `claude-opus-4-8`). `.env.example` posé ; `/healthz` expose le **mode** (off/api), jamais la clé ; le générateur rejoint l'état de l'app (R5.2 le consommera).
- 5 tests ajoutés (19 pytest au total) : défaut extractif, refus de boot api-sans-clé, mode inconnu refusé, clé/modèle lus de l'env, invite pure numérotant les extraits. Vérifié en réel : boot refusé sans clé, `GenerateurAPI` construit avec le SDK, healthz sans secret.

**Décisions**

- La clé vit en variable d'environnement (Dokploy secrets), pas en base ni dans l'UI — cohérent avec `JWT_SECRET` et ADR-002.

**Prochaine étape** : R5.2 — assistant au contrat (proxy NestJS), le mode extractif d'abord, la rédaction branchée sur ce `Generateur`. Restes : recette R4 sur appareil, redéploiement Dokploy de `release/r3`.

---

## 2026-07-17 — Pr. Daaif (+ Claude) — R5.1 : socle `apps/ai` — ingestion anonymisée + recherche sémantique

**Actions**

- **ADR-004 actée** : embeddings **locaux sur CPU** (fastembed ONNX, `paraphrase-multilingual-MiniLM-L12-v2`, 384 dims — les textes du client ne quittent jamais le serveur), pgvector dans le Postgres existant (table `RagChunk` **possédée par Prisma**, migration `r5_ia` + état de corpus sur Document), génération **opt-in** (`AI_GENERATION=off` par défaut : mode extractif honnête, la recette doit passer sans clé API), suggestion de bilan sans LLM (similarité sémantique, explicable). Topologie : `siop2-ai` jamais exposé, joint par l'API NestJS seule (`X-Service-Token`).
- **`apps/ai` posé** (FastAPI + uv, python 3.11) : config validée au démarrage, `/healthz`, `/internal/reindex` (idempotent) et `/internal/search` protégés par le jeton de service. **Pipeline** : PDF MinIO → texte par page (pypdf) → **anonymisation D4** (e-mails, téléphones marocains, noms connus de la base — insensible casse/accents, fonction pure) → découpage (~900 car., chevauchement, testé) → embeddings → `RagChunk` avec localisateur (« p. 42 », « bilan du 17/07 »). Bilans codés clôturés ingérés aussi (« sur votre parc… »). L'exclusion de corpus (D3) s'applique à l'ingestion ET à la lecture.
- **14 pytest verts + ruff** (embeddeur **déterministe** en test/CI — aucun téléchargement, même interface 384 dims) ; **job CI `ai`** (uv), deploy en dépend.
- **Vérifié en réel avec le vrai modèle ONNX** : réindexation du corpus seedé en 7 s (1 PDF réel → 31 extraits paginés, 3 bilans), recherche sémantique concluante (PDF trouvé par le sens, bilans du parc par « frottement des guides »), e-mails du PDF remplacés par ⟨contact⟩, **0 identité dans les chunks** (contrôle SQL sur les 9 noms seedés).

**Décisions**

- Convention de test R5 : pytest unitaires purs en CI (sans DB ni réseau) ; l'intégration réelle (DB + MinIO + modèle) se vérifie en local et en recette.

**Prochaine étape** : R5.2 — l'assistant au contrat (proxy NestJS authentifié → `siop2-ai`, mode extractif sourcé « sourcé ou silencieux ») + suggestion de codes de bilan. Restes : recette R4 sur appareil, redéploiement Dokploy de `release/r3`.

---

## 2026-07-17 — Pr. Daaif (+ Claude) — R5 ouverte : maquettes IA (design d'abord)

**Actions**

- **`maquette-r5.html`** (docs/02-design/maquettes) : 6 écrans — **Assistant sourcé** (recette type du cadrage : « couple de serrage des guides Gen2 ? » → réponse à citations numérotées, sources document/page/**extrait exact** ouvrables), **Sans source = refus** (l'anti-hallucination visible : « je préfère ne pas inventer » + ce qui a été cherché + action utile), **Suggestion de bilan** web et mobile (description libre → codes EXISTANTS des référentiels, justification + confiance, « Appliquer » pré-remplit avec liseré « suggéré », rien d'enregistré sans le geste humain), **Corpus & ingestion** (bibliothèque R3 + bilans codés, statut par document, exclusion réversible, bandeau 09-08), **Voix** (option « si budget » : dictée opt-in, transcription relue, audio purgé immédiatement). Bi-thème, tokens répliqués, vérifiée en navigateur (6 onglets + sombre, zéro erreur console).
- Artefact de validation publié (6 écrans + preuve bi-thème + **5 décisions à acter** : D1 aucune écriture automatique, D2 sourcé ou silencieux, D3 corpus fermé et visible, D4 anonymisation à l'ingestion 09-08, D5 voix opt-in avec purge).

**Décisions**

- **→ Levé le 17/07/2026 : maquettes R5 et les 5 décisions (D1-D5) VALIDÉES par le référent.** Lancement R5.1 (socle `apps/ai` + ADR-004).

**Prochaine étape** : R5.1 — ADR-004 (embeddings/modèles), migration `r5_ia` (chunks pgvector, état de corpus), pipeline d'ingestion anonymisé et testé. Restes : recette R4 sur appareil, redéploiement Dokploy de `release/r3`.

---

## 2026-07-17 — Pr. Daaif (+ Claude) — R4 CLOSE : tag `release/r4` (recette téléphone reportée)

**Actions**

- Tag annoté **`release/r4`** posé et poussé (CI verte sur `c8b3c17` : 74 tests API, 17 jest-expo, 14 Playwright).

**Décisions**

- **Décision du référent : la recette sur téléphone (Expo Go — vrai mode avion, scan caméra) est REPORTÉE**, il y accédera plus tard avec un appareil. Le tag est posé sur la foi de la recette « mode avion » rejouée 13/13 en Expo web piloté (toute la mécanique D1/D2 vérifiée, capteurs exclus). **La validation terrain reste due avant toute mise en production client de l'app mobile** — notée comme reste de release.

**Prochaine étape** : ouverture **R5 — IA** (design d'abord : maquettes assistant RAG sourcé + suggestion de codes de bilan, décisions à acter). Restes : recette R4 sur appareil, redéploiement Dokploy de `release/r3`.

---

## 2026-07-17 — Pr. Daaif (+ Claude) — R4.3 : file d'écriture, verrou optimiste, Synchro & conflits

**Actions**

- **Verrou optimiste côté serveur (D2)** : `updatedAt` exposé au détail OT ; `baseUpdatedAt` optionnel sur transition/coche/bilan → **409 « Conflit de version » contextualisé** (qui, quand — depuis le dernier événement). Toute écriture « secondaire » (coche, bilan, commentaire, conso, MO) fait désormais avancer la version, sinon le verrou serait aveugle. Sans version fournie, comportement web inchangé. 4 tests e2e dédiés (74 au total).
- **File d'écriture mobile (D1)** : store persisté AsyncStorage (survit au redémarrage, un ENVOI interrompu redevient rejouable), rejeu **dans l'ordre** ; succès → sortie de file **et propagation de la version fraîche** aux saisies restantes du même OT (nos propres écritures ne se conflictent pas entre elles — un écart étranger reste détecté) ; coupure → tout reste en attente ; refus → CONFLIT et la file s'arrête là. Transitions, coches, bilan et **photos (D5** : compressées ~1 600 px, expo-image-picker/manipulator**)** passent par la file, avec patch optimiste du cache (l'OT affiche « Terminé » localement, chips « en file » partout).
- **Écran Synchro & conflits** (écran 7) : file visible et horodatée, badge tabbar (ambre → rouge si conflit), carte de conflit avec le message de l'API et les trois choix — voir l'OT, **rejouer sur la version à jour**, abandonner. Préchargement du parc et des référentiels à l'entrée (trou D1 débusqué par la vérif : le bilan hors-ligne n'avait pas ses vocabulaires).
- **Sécurité & routage (demande du référent) : ADR-003** — qui vit où sur l'appareil et ce qui le protège (jeton en trousseau, cache/file en sandbox, purge complète à la déconnexion), l'API seule autorité, deep links `siop://` jamais suivis aveuglément (le scan extrait et résout localement). **Correctif réel au passage : la file et le cache persisté n'étaient pas purgés au logout** — un autre compte sur le même téléphone aurait pu rejouer les saisies du précédent.
- **Recette « mode avion » rejouée en Expo web piloté : 13/13** — OT ouvert, avion, Démarrer + bilan + clôture hors-ligne (état local immédiat, 3 en file), Salma modifie l'OT pendant ce temps, retour réseau → rejeu auto → **conflit tranché par l'humain** → file vide → serveur : Terminé avec bilan. 17 tests jest-expo (5 sur la file), zéro erreur console inattendue.

**Décisions**

- La recette officielle « mode avion en sous-sol » reste à rejouer **sur téléphone** (Expo Go) avec le référent — le web a validé toute la mécanique, pas les capteurs ni le vrai mode avion.
- Durcissements notés à l'ADR-003 : chiffrement applicatif de la file et épinglage TLS si exigence client.

**Prochaine étape** : recette R4 sur appareil avec le référent → tag `release/r4` → R5 IA (design d'abord). Redéploiement Dokploy de `release/r3` toujours en attente côté serveur.

---

## 2026-07-17 — Pr. Daaif (+ Claude) — R4.2 : scan QR, fiches terrain, grille cochable

**Actions**

- **Scanner réel** (expo-camera, QR seulement) : analyse du code testée (`analyseScan` — URL portail `…/q/REF` de l'étiquette A6 quel que soit le domaine, ou référence tapée ; les QR étrangers sont refusés proprement, jamais d'écran blanc). **Résolution D4 : le parc en cache d'abord** (fonctionne en sous-sol), rafraîchissement seulement si le réseau est là ; référence inconnue → message honnête. Repli saisie manuelle (seul chemin sur web, assumé).
- **Fiche ascenseur** (consultation, D3) : identité, organes, interventions **visibles par le rôle** (invariant « voir autre »), raccourci vers l'OT en cours. **Fiche OT** : un bouton principal selon la machine à états, pièces & main-d'œuvre aux coûts figés, garde de clôture alimentée par les `closureBlockers` de l'API (une seule source de vérité). **Clôture terrain** : bilan codé 6 champs (sélecteur plein écran au pouce — RN n'a pas de `<select>`), garde visible, clôture en ligne. **Préventif** : mes grilles → checklist cochable, appui long = N/A, coche individuelle à l'API.
- En R4.2 les écritures restent **en ligne** (bandeaux explicites hors-ligne) — la file générale et le verrou optimiste sont le cœur de R4.3, comme prévu.
- Vérifié en Expo web piloté : **12/12** — scan A1 → fiche → OT-0341 (505 MAD, garde), référence inconnue, coche/décoche (état restitué), et un OT créé par l'API : Démarrer → clôture bloquée bilan incomplet → 3 champs remplis → garde éteinte → **Terminé** ; zéro erreur console ; données de test purgées. 12 tests jest-expo (scan, progression, tri, tokens).

**Décisions**

- Leçon accessibilité RN web : `accessibilityState.checked` ne produit PAS `aria-checked` — utiliser la prop `aria-checked` (mieux pour les lecteurs d'écran, et testable).
- La caméra ne se recette pas sur web : scan réel à valider dans Expo Go (le repli manuel couvre le parcours en attendant).

**Prochaine étape** : R4.3 — file d'écriture persistée rejouée dans l'ordre, verrou optimiste tranché par l'humain (D2), écran Synchro & conflits, photos en file (D5) → recette « mode avion » sur téléphone. Redéploiement Dokploy de `release/r3` toujours en attente.

---

## 2026-07-17 — Pr. Daaif (+ Claude) — R4.1 : socle mobile Expo (app du technicien)

**Actions**

- **`apps/mobile` posé** (Expo SDK 57, TypeScript strict, pnpm workspace, `@siop/shared` consommé) : connexion e-mail/mdp + **sélecteur démo ADR-002** (n'existe que si l'API l'expose), coquille **tabbar** de la maquette (Scanner/Préventif/Synchro visibles mais marqués R4.2/R4.3 — même patron que la sidebar web), **Ma journée** : OT triés priorité puis échéance (fonction pure testée), urgence « personne bloquée » en tête, strie de priorité, pastille de synchro permanente.
- **D1 en actes (lecture)** : cache TanStack **persisté** dans AsyncStorage (7 jours), NetInfo → `onlineManager` + bandeau hors-ligne horodaté. Jeton en SecureStore. Client typé régénéré depuis `docs/openapi.json` (règle d'or). Thème : tokens light/dark répliqués (testés), Manrope embarquée.
- **API : `CORS_ORIGINS`** (env, vide par défaut = pas de CORS) — nécessaire à Expo **web**/debug seulement ; le web de prod reste derrière le proxy nginx, les apps natives n'ont pas d'Origin.
- Vérifié en navigateur réel (Expo web + Playwright) : **10/10** — connexion démo Ahmed → Ma journée (ses OT seulement : le scoping « voir autre » joue), bascule hors-ligne (bandeau + liste servie du cache) et retour, onglets à venir honnêtes ; zéro erreur console. 6 tests jest-expo, lint racine étendu (react-hooks sur mobile), **job CI `mobile`** ajouté (deploy dépend de lui).

**Décisions**

- La vraie recette hors-ligne (mode avion, redémarrage, file d'écriture) se fera **sur téléphone** — Expo web ne prouve que le chemin de code NetInfo→UI (dispatch `navigator.connection.change` observé).
- Leçon Playwright : `setOffline` ne déclenche pas les événements réseau du navigateur — NetInfo web écoute `navigator.connection` sur Chromium.

**Prochaine étape** : R4.2 — scan QR (expo-camera) + fiche ascenseur + fiche OT + checklist cochable ; puis R4.3 file d'écriture + verrou optimiste (D2). Redéploiement Dokploy de `release/r3` toujours en attente.

---

## 2026-07-17 — Pr. Daaif (+ Claude) — R4 ouverte : maquettes mobile (design d'abord)

**Actions**

- **`maquette-r4.html`** (docs/02-design/maquettes) : 7 écrans de l'app technicien — Ma journée (en ligne ET hors-ligne côte à côte), Fiche OT, Clôture terrain (bilan codé R2, garde visible ; variante « en file » hors-ligne), Scan QR (étiquettes A6 de R1, hors-ligne inclus), Fiche ascenseur (consultation devant la machine), Checklist du mois (coches individuelles en file), **Synchro & conflits** (file visible, conflit de verrou optimiste où l'humain tranche). Tokens répliqués, bi-thème, gabarit téléphone repris de la maquette R3, données du parc seedé. Vérifiée en navigateur (7 onglets + bascule sombre, zéro erreur console).
- Artefact de validation publié pour le référent (7 écrans + preuve bi-thème + **5 décisions à acter** : D1 offline-first lecture locale/écriture en file, D2 verrou optimiste tranché par l'humain, D3 périmètre fermé « app du technicien » sans création d'OT mobile, D4 scan local des étiquettes R1, D5 photos compressées en file — pas d'audio ni de géolocalisation en R4, loi 09-08).

**Décisions**

- **→ Levé le 17/07/2026 : maquettes R4 et les 5 décisions (D1-D5) VALIDÉES par le référent.** Lancement R4.1 (socle mobile Expo).

**Prochaine étape** : R4.1 — socle Expo : coquille tabbar, connexion + sélecteur démo (ADR-002), client typé du contrat, « Ma journée » en lecture avec cache hors-ligne. En parallèle, redéploiement Dokploy de `release/r3` toujours à faire.

---

## 2026-07-17 — Pr. Daaif (+ Claude) — R3 CLOSE : tag `release/r3`

**Actions**

- Recette R3 prononcée par le référent après la revue pixel corrigée (8 écarts + recherche globale, artefact à jour, CI verte sur `460ef4a`).
- Tag annoté **`release/r3`** posé et poussé.

**Décisions**

- Le tag précède le redéploiement (webhook CD toujours absent) : le déploiement Dokploy se fait manuellement depuis ce tag, puis vérification en ligne — même séquence que R1/R2.

**Prochaine étape** : redéployer sur Dokploy (migration `r3_recette_fixes` + seed au boot), vérifier en ligne (rattachements Tiers, recherche ⌘K, période des stats), puis ouvrir **R4 Mobile** (Expo — design d'abord, maquettes avant tout code).

---

## 2026-07-17 — Pr. Daaif (+ Claude) — R3.4 : revue pixel, corrections de recette, recherche globale

**Actions**

- **Revue pixel R3** (artefact partagé, captures maquette ↔ appli côte à côte, 1440 px, thème clair) : 7/7 écrans structurellement fidèles, 8 écarts relevés. **Arbitrage du référent : tout corriger + activer la recherche.**
- **Stock** : filtre Fournisseur, « Entrée de stock » depuis la liste (choix de la pièce dans la modale), pièces sous le seuil en tête. **Fiche pièce** : fournisseur → lien vers Tiers. **Statistiques** : « Période » devient un vrai sélecteur 3/6/12 mois (`months` au contrat, libellés et barres suivent). **Tiers** : les syndics affichent leurs sites — nouveau lien site→client/syndic (migration `r3_recette_fixes`, `Location.partnerId`, garde « CLIENT seulement »), éditable sur la fiche site, seedé (Tour Atlas, Résidence Al Manar). **Bibliothèque** : filtre « Rattaché à » + zone de glisser-déposer (la modale s'ouvre préremplie, le rattachement reste obligatoire).
- **Recherche globale ⌘K active** : `GET /search` (73e opération au contrat) — chaque famille (OT/ascenseurs/sites) n'est interrogée que si le rôle a la permission `view` du domaine, les OT respectent « voir autre » ; topbar avec debounce 200 ms, résultats groupés, navigation flèches + Entrée. 8 tests API dédiés (scoping vérifié rôle par rôle).
- Écart « regroupement des tiers » retiré : le tri type-puis-nom existait déjà (constat d'écran, pas de code).
- Vérifié : 18/18 contrôles en navigateur réel (zéro erreur console), **14/14 Playwright** (dont nouveau parcours « recette corrigée »), **74 tests API** (13 suites). Artefact de revue mis à jour (statut « corrigé », captures fraîches).

**Décisions**

- « Bons de commande » reste dans l'entête du Stock **en plus** de la maquette : c'est le seul accès à l'écran BC — à entériner en recette (ou déplacer si le référent préfère).
- La liste des tiers restant sous `PURCHASE_ORDERS.view`, le champ syndic de la fiche site n'est éditable que si le rôle voit les tiers (sinon lecture seule).

**Prochaine étape** : validation finale du référent sur l'artefact → déploiement Dokploy → tag `release/r3` → R4 Mobile (design d'abord).

---

## 2026-07-16 — Pr. Daaif (+ Claude) — R3.3+ : aperçu et ouverture des documents (retour de pré-recette)

**Actions**

- **Retour du référent** : les documents téléversés n'avaient ni aperçu ni ouverture (seul « Télécharger » existait). Corrigé dans la vignette (`carte-documents.tsx`) : les **images affichent leur vrai aperçu** (le fichier exige le jeton → récupération du blob puis URL objet révoquée au démontage, jamais de `<img src>` nu) ; les PDF gardent leur pictogramme.
- **« Ouvrir »** : nouveau bouton (et clic sur la vignette) — onglet ouvert **dans le geste utilisateur** (sinon bloqueurs de pop-up) puis pointé sur le blob ; le navigateur affiche PDF et images dans sa visionneuse. « Télécharger » inchangé.
- Vérifié en navigateur réel (Playwright, appli lancée) : vignette image réellement chargée (`naturalWidth > 0`), ouverture image et PDF en onglet `blob:` (PDF rendu dans la visionneuse Chrome — capture), téléchargement intact, suppression OK, zéro erreur console. Typecheck + vitest verts.

**Décisions**

- L'aperçu télécharge le fichier entier (pas de miniature côté serveur) : acceptable au volume actuel ; si la bibliothèque grossit, générer des miniatures à l'upload (noté pour le durcissement).

**Prochaine étape** : recette R3 avec le référent (revue pixel), déploiement Dokploy, tag `release/r3` → puis R4 Mobile (design d'abord).

---

## 2026-07-16 — Pr. Daaif (+ Claude) — R3.3 : les écrans web de la gestion — R3 prête pour recette

**Actions**

- **7 écrans fidèles à `maquette-r3.html`** : Stock (alerte sous seuil, strie ambre, « Préparer le BC » sur la ligne), Fiche pièce (« les mouvements SONT le stock », entrée/ajustement motivé), Bons de commande (liste + panneau, envoi/annulation/**réception → entrées de stock**), création de BC **préremplie** depuis l'alerte (fournisseur de la pièce, quantité pour repasser le seuil + 5, dernier PU), Tiers, Bibliothèque (upload rattaché à un appareil, filtre par type), Statistiques (4 KPI, pannes par organe **depuis les bilans codés**, coûts par mois, top équipements).
- **La fiche OT s'achève** : carte « Pièces & main-d'œuvre » réelle (consommer une pièce — prix figé annoncé AVANT le clic, impact stock affiché ; saisir du temps au taux figé) + carte « Documents » réelle (photo d'intervention téléversée/retéléchargée). La fiche ascenseur gagne la même carte Documents ; le tableau de bord allume « Pannes par organe » ; **Personnes affiche et édite le taux horaire**.
- Nav « Ressources »/« Statistiques » activée par la matrice (PARTS/PURCHASE_ORDERS/ASSETS/ANALYTICS.view) ; captures des 9 écrans comparées aux maquettes (bi-thème), zéro erreur console.
- **Recette R3 automatisée** (2 parcours Playwright, données E2E autonomes purgées au setup) : tiers → pièce → sous seuil → BC prérempli → réception → **l'alerte s'éteint** ; OT : consommation + 1 h 30 → **380 MAD** exacts ; upload/download MinIO réel ; statistiques lues par la Vue seule. **13/13 e2e verts, 10,7 s.**

**Décisions**

- Le formulaire BC prérempli attend la fin du refetch avant d'initialiser ses lignes (le cache TanStack portait un stock périmé — attrapé par la recette, pas par l'œil).
- `cleanup-e2e` purge désormais les entités R3 (mouvements liés aux OT E2E supprimés AVANT les OT, sinon ils deviennent orphelins et faussent les stocks seedés d'un run à l'autre).

**Prochaine étape** : recette R3 avec le référent (revue pixel), déploiement Dokploy, tag `release/r3` → puis R4 Mobile (design d'abord).

---

## 2026-07-16 — Pr. Daaif (+ Claude) — R3.2 : bibliothèque de documents + analytics

**Actions**

- **`FileStorage` gagne ses vraies opérations** (put/stream/remove ; bucket créé au démarrage — aucune étape manuelle au déploiement) ; MinIO reste confiné à son implémentation (règle ESLint).
- **Bibliothèque** : upload multipart (PDF/JPG/PNG, 20 Mo max, rattachement appareil OU OT **requis**, permission d'édition sur la cible), liste filtrable, **téléchargement streamé par l'API** (MinIO jamais exposé — topologie du runbook respectée), suppression. Test e2e : les octets téléchargés sont IDENTIQUES aux octets envoyés.
- **Analytics** (`GET /analytics/summary`, permission ANALYTICS) : tout est dérivé du réel — coûts par mois depuis les mouvements/main-d'œuvre figés, **pannes par organe depuis les bilans codés**, taux de préventif (grilles terminées/générées), durée moyenne de résolution, top équipements en coût.
- Générateur OpenAPI étendu (query params, multipart, réponse binaire) — 71 opérations. **CI : service MinIO ajouté** (jobs api + e2e, image bitnami).
- **58 tests verts** (92 % / 73,9 %) ; test de régression du tri des « Interventions récentes » rendu **déterministe** (les positions absolues étaient instables sous 12 suites parallèles) — 6 runs complets consécutifs verts.

**Prochaine étape** : R3.3 — écrans web (stock, fiche pièce, BC, coûts sur fiche OT, statistiques, tiers, bibliothèque, taux dans Personnes), puis recette R3 + déploiement + tag.

---

## 2026-07-16 — Pr. Daaif (+ Claude) — R3.1 : socle backend de la gestion

**Actions**

- **Maquettes R3 et les 4 décisions VALIDÉES par le référent** → lancement du socle.
- **Modèle** (migration `r3_gestion`, 7 tables) : Partner, Part (SANS colonne de quantité), StockMovement (signé, tracé, PU figé), PurchaseOrder/Line, LaborTime (taux figé), Document (préparé pour R3.2) ; `User.hourlyRate` (taux courant, administrable dans Personnes).
- **API** (66 opérations au contrat) : tiers, pièces (stock = Σ mouvements calculé en `groupBy`, alerte sous seuil), entrée/ajustement (motif requis, **stock jamais négatif** — vérifié en transaction), BC (Brouillon→Envoyé→Reçu ; **la réception crée les RECEIPT et met à jour `lastUnitPrice`**), consommation sur OT (stock suffisant + **prix figé**) et main-d'œuvre (**taux figé**, refus motivé si taux non défini) ; `WorkOrderDetail.costs` (lignes + total immuables) ; coûts verrouillés après clôture/annulation (409).
- **Seed** : 5 tiers, 5 pièces de la maquette (P-0113 sous seuil via son histoire de mouvements), BC reçu + BC envoyé, taux horaires, et **la carte maquette rejouée : OT-0341 = 505 MAD** (240 + 85 + 180) — vérifiée par test.
- **55 tests verts** (92 % / 74,9 %) dont la recette officielle : consommer sous seuil → alerte → BC → réception → réappro, prix d'hier intact sur l'OT pendant que le prix courant change ; hausse du taux d'Ahmed sans effet sur les OT passés.

**Leçon majeure (durcissement)**

- Les références « max+1 » (OT/DEM/BC/P) étaient une **course sous charge parallèle** (500 sporadiques malgré les retries). Remplacées par des **séquences Postgres** (`nextval`) initialisées au max existant : la classe de bugs disparaît — 3 runs Jest complets consécutifs verts. La numérotation ne se remet pas à zéro chaque année (l'unicité prime), consigné.

**Prochaine étape** : R3.2 — bibliothèque de documents (upload multipart 20 Mo → `FileStorage.putObject`, téléchargement streamé via l'API — MinIO jamais exposé) + analytics (`GET /analytics/summary`), puis R3.3 écrans web.

---

## 2026-07-16 — Pr. Daaif (+ Claude) — R3 ouverte : maquettes de la gestion à valider

**Actions**

- **R3 « Gestion » ouverte — design d'abord** : `maquette-r3.html`, **7 écrans** dans le moule validé : Stock (dérivé des mouvements, alertes sous seuil avec « Préparer le BC »), Fiche pièce (les mouvements SONT le stock), Bons de commande (Brouillon → Envoyé → Reçu ; la réception crée les entrées), Coûts sur OT (la carte validée R0 devient réelle : consommation à **prix figé**, main-d'œuvre à **taux figé**), Statistiques (coûts, pannes par organe alimenté par les bilans codés, taux de préventif, top équipements — palette CVD), Tiers (annuaire minimal fournisseurs/syndics), Bibliothèque (documents typés rattachés appareil/OT, MinIO via FileStorage — futur corpus RAG R5).
- Rendu vérifié (7 écrans + thème sombre, zéro erreur).

**Décisions (proposées à la validation)**

- **Stock strictement dérivé des mouvements** (décision v1 éprouvée) : aucune saisie directe de quantité ; les ajustements d'inventaire sont des mouvements motivés.
- **Prix figé à la consommation** (dernier prix d'achat) et **taux horaire figé** par personne (champ géré dans Personnes) : le coût d'un OT ne bouge plus jamais.
- La réception d'un BC crée les mouvements d'entrée et fige le PU.
- Documents : types fermés (Notice / Certificat / Photo / Autre), 20 Mo max, rattachement obligatoire (appareil ou OT).

**⛔ Bloquant** : validation des maquettes R3 par le référent avant toute ligne de code R3.

---

## 2026-07-16 — Pr. Daaif (+ Claude) — 🏁 R2 CLOSE : recettée, déployée, taguée

**Actions**

- **Recette R2 prononcée par le référent** (une anomalie détectée et corrigée en cours de recette — voir entrée précédente : tri des « Interventions récentes »).
- **Déploiement vérifié en ligne** : portail public `/q/A1` opérationnel (résolution QR 200), 13 OT avec l'urgence en tête, préventif de juillet généré sur l'instance (8 grilles, 7 premiers contrôles).
- DoD complète → **tag `release/r2`**. Le carnet papier est remplacé : signalement QR sans compte → OT → bilan codé → clôture gardée → suivi, plus préventif idempotent et compteurs.

**Prochaine étape** : R3 Gestion — design d'abord : maquettes stock/BC/tiers/coûts OT/analytics/bibliothèque à produire et faire valider.

---

## 2026-07-16 — Pr. Daaif (+ Claude) — Recette R2 : anomalie du référent corrigée

**Anomalie (recette du référent)** : un OT créé, assigné à Ahmed, traité et clôturé apparaissait dans le tableau de bord d'Ahmed mais PAS dans celui de Salma ni de l'administrateur.

**Diagnostic** : la liste API triait **par statut d'abord** (les Terminés relégués en queue) et le tableau de bord affiche les 5 premières lignes. Ahmed, sans « voir autre », a une liste courte → son OT clôturé restait dans le top 5 ; Salma et l'admin voient tout le parc → l'OT clôturé était éjecté des « Interventions récentes ». (La liste OT filtrait aussi « actifs » par défaut, conforme à la maquette — c'est le tableau de bord qui trompait.)

**Correctif** : la liste trie par **dernière activité** (`updatedAt` desc) — un OT qui vient d'être clôturé remonte en tête pour tout le monde ; les urgences « personne bloquée » **actives** restent épinglées au sommet (un OT d'urgence annulé ne squattait plus la tête, corrigé au passage). Reproduit avant / vérifié après sur le scénario exact du référent ; **test de régression** ajouté à la recette e2e (position de l'OT clôturé dans la liste de Salma ≤ nombre d'urgences actives). 50 tests API + 11 Playwright verts.

---

## 2026-07-16 — Pr. Daaif (+ Claude) — R2.4 : portail public QR — l'étiquette prend vie

**Actions**

- **Trois routes publiques `/portal`** (les seules routes @Public métier de l'API), **throttlées** (`@nestjs/throttler` : 10 signalements/min, 60 lectures/min — première surface sans compte) : résolution du QR (`référence → appareil`), signalement (retourne un **jeton de suivi opaque**), suivi par `référence + jeton` (pas de jeton, pas de lecture — aucune énumération possible).
- **Suivi sans compte** : `Request.publicToken` (migration `r2_portail`) ; le téléphone du gardien garde `[référence, jeton]` en localStorage (10 max) ; étapes sans jargon **Reçu → Intervention → Résolu** dérivées de l'OT lié ; un rejet s'affiche « Sans suite : {motif} ».
- **Page `/q/{réf}`** fidèle à l'écran 6 validé R0 (mobile d'abord) : équipement prérempli « détecté par le QR », gros interrupteur personne bloquée, description, photo annoncée (upload → R3), messages d'erreur en français métier (QR inconnu, throttling).
- **e2e Playwright** : LA boucle produit — le gardien signale sans compte sur `/q/A1`, l'équipe traite (approbation → démarrage → bilan → clôture via API), le gardien recharge et voit ✓ Résolu. 11 tests Playwright verts, 50 tests API, 52 opérations au contrat. Générateur OpenAPI : les paramètres non-`id` ne sont plus typés uuid.

**Décisions**

- Le portail n'expose JAMAIS de liste : lecture uniquement par jeton individuel.
- Throttling au niveau du contrôleur portail seulement (le reste de l'API est derrière JWT).

**R2 est fonctionnellement complète** (OT, bilan codé, demandes, portail QR, préventif, compteurs). Prochaine étape : recette R2 avec le référent (revue pixel ↔ maquettes R0+R2), déploiement, tag `release/r2`.

---

## 2026-07-16 — Pr. Daaif (+ Claude) — R2.3 : écrans web de l'exploitation

**Actions**

- **7 écrans/évolutions fidèles aux maquettes validées** : Liste OT (filtres, strie rouge, « immédiat »), **Fiche OT** (boutons de transition issus d'`allowedTransitions`, clôture grisée avec l'explication de la garde, bilan codé 6 selects alimentés par les référentiels, checklist Fait→N-A→à faire au clic, activité chronologique + commentaires, assignation, annulation motivée), Nouvel OT (interrupteur « personne bloquée » qui force la priorité), Demandes (table + panneau d'approbation, rejet en modale à motif), Préventif (4 tuiles réelles, bouton générer avec résumé « regénérer ne double rien », gabarits administrables), Compteurs (saisie + historique), **Tableau de bord réel** (bandeau urgence cliquable, KPIs, OT par statut, interventions récentes ; coûts et pannes par organe annoncés R3).
- **L'urgence traverse l'app** : chip pulsante dans la topbar (rafraîchie 60 s), badges de nav (urgences OT, demandes à traiter), bandeau dashboard, tête de liste. Fiche appareil : historique réel + accès compteurs. Accueil dédié aux rôles sans exploitation (Demandeur).
- **Trou de conception débusqué par l'e2e** : le Demandeur n'a pas `ASSETS.view` → son sélecteur d'équipement était vide. Correctif : `GET /assets/options` (authentification seule — les références sont affichées en cabine). 49 opérations au contrat.
- **Deux fragilités réelles corrigées dans la génération du préventif** : collision de référence (course avec une création d'OT) désormais RETENTÉE au lieu de sauter silencieusement un appareil ; appareil supprimé entre lecture et écriture toléré (P2003). 3 runs Jest complets consécutifs verts.
- **9 tests Playwright verts** dont la recette R2 officielle : Karim signale → Salma approuve+assigne → OT → démarrage → clôture bloquée visible → bilan → clôture → la demande de Karim passe « Résolue » ; + préventif idempotent via l'UI, urgence bout-en-bout, compteur croissant. 50 tests API (94,6 % / 78,9 %).

**Prochaine étape** : R2.4 — portail public QR (`/q/{ref}` → signalement prérempli sans compte, suivi sans jargon — écran validé R0), puis recette R2 + déploiement + tag.

---

## 2026-07-16 — Pr. Daaif (+ Claude) — R2.2 : préventif (génération idempotente) + compteurs

**Actions**

- **Modèle** (migration `r2_preventif_compteurs`) : `Asset.underContract` (contrat préventif, maquette fiche appareil) et `WorkOrder.periodKey` avec **unicité `[assetId, periodKey]` : l'idempotence de la génération est EN BASE**, pas seulement dans le code — deux générations concurrentes ne peuvent pas doubler une grille.
- **Génération mensuelle** (`POST /preventive/generate`, 200 idempotent) : une grille par appareil sous contrat ; tâches dues par périodicité **ancrée sur la mise en service** (mensuelles toujours dues) ; appareil sans historique préventif → OT « **Premier contrôle** » avec toutes les tâches ; échéance = fin de mois ; événement GENERATED tracé. Déclenchement manuel en R2 (bouton maquette) — l'automatisation cron/BullMQ viendra avec le durcissement production.
- Gabarits administrables (ajout/renommage/désactivation — un gabarit désactivé sort des générations suivantes) ; `GET /preventive/status` (générées, terminées, en retard, premiers contrôles du mois).
- **Compteurs** : `GET /assets/{id}/meters` (les 2 compteurs, relevés récents d'abord) ; `POST /assets/{id}/meter-readings` — **relevé strictement croissant**, refus motivé sinon.
- Contrat : +7 opérations (48). **50 tests verts** (couverture 94,7 % / 78 %) : idempotence (8 grilles seed, jamais 16), premier contrôle vs grille normale, mois anniversaire (mars 2031 pour A1 : les 3/6/12 mois tombent ensemble), gabarit désactivé exclu, compteur qui refuse de redescendre. Smoke test prod : juillet 2026 généré (9 grilles, 8 premiers contrôles), 2ᵉ appel à zéro.

**Leçon**

- Les specs Jest partagent la base en parallèle : les assertions e2e doivent porter sur **leurs propres données** (ici les 8 appareils du seed), jamais sur des comptages globaux.

**Prochaine étape** : R2.3 — écrans web de l'exploitation (liste/fiche OT, demandes+approbation, nouvel OT, préventif, checklist, compteurs, chip urgence + bandeau tableau de bord).

---

## 2026-07-16 — Pr. Daaif (+ Claude) — R2.1 : socle backend de l'exploitation

**Actions**

- **Modèle R2** (doc + migration `r2_exploitation`, 10 tables) : WorkOrder (référence séquentielle, horodatages par état), WorkOrderEvent (activité), Request (1-1 vers OT, motif de rejet), ReferenceValue (référentiels du bilan par champ), InterventionReport (6 FK nommées), TaskTemplate/ChecklistItem, Meter/MeterReading.
- **Contrat** : 15 opérations (41 total) — la **table des transitions** et les champs requis du bilan vivent dans `@siop/shared` (une seule loi pour l'API et l'UI) ; la fiche OT expose `allowedTransitions` et `closureBlockers` (messages métier).
- **API** : machine à états stricte (transition → DONE refusée si bilan incomplet OU checklist avec tâche sans réponse — messages « quoi faire », charte §7) ; approbation de demande = création d'OT lié en 1-1 (double traitement → 409) ; rejet à motif obligatoire ; **scoping « voir autre »** appliqué aux listes ET aux accès directs (404, pas de fuite) ; valeurs de bilan validées champ par champ.
- **Seed** : 31 valeurs de référentiels (6 champs), 8 gabarits de préventif (parachute réglementaire), 4 OT et 4 demandes rejouant la maquette, compteurs d'A1.
- **45 tests verts** (couverture 95 % / 79 % branches) dont la **recette officielle rejouée** : demande Karim → approbation Salma → OT assigné Ahmed → démarrage → clôture refusée sans bilan → bilan codé → clôture → Karim voit sa demande résolue. Smoke test build prod.

**Décisions**

- `POST` de transition/rejet répondent **200** (le contrat prime sur le défaut 201 de Nest — c'est le contrat qui a raison).
- La priorité « personne bloquée » trie en tête côté API : aucun client ne peut l'oublier.

**Prochaine étape** : R2.2 — génération mensuelle du préventif (BullMQ, idempotente, premier contrôle) + API compteurs, puis R2.3 écrans web.

---

## 2026-07-16 — Pr. Daaif (+ Claude) — R2 ouverte : maquettes des écrans manquants à valider

**Actions**

- **R2 « Exploitation » ouverte — design d'abord.** Les écrans cœur sont déjà validés depuis R0 (tableau de bord, liste OT, fiche OT avec bilan codé 6 champs et garde de clôture, portail demandeur) ; `maquette-r2.html` complète avec les **5 écrans manquants** : Demandes (file + panneau d'approbation : approuver → OT 1-1, rejeter avec motif requis), Nouvel OT (interrupteur « personne bloquée » explicite), Préventif (gabarits à périodicité — parachute marqué réglementaire — génération du mois **idempotente**, premiers contrôles), OT préventif (checklist Fait/N-A, clôture bloquée si tâche sans réponse), Compteurs (relevés croissants, historique).
- Rendu vérifié (5 écrans + thème sombre, zéro erreur).

**Décisions (proposées à la validation)**

- Statuts de DEMANDE distincts (Reçue / Approuvée→OT lié / Rejetée / Résolue) réutilisant la sémantique chromatique des statuts OT ; à trancher : est-ce une entorse à « réservés » de la charte ou une extension cohérente ?
- Un rejet de demande **exige un motif** (lisible côté portail, sans jargon).
- Génération mensuelle : automatique le 1ᵉʳ à 06 h + bouton manuel ; **regénérer ne crée que le manquant** ; appareil sans historique → OT « premier contrôle » (toutes les tâches).
- Relevé de compteur strictement croissant (refus sinon).

**⛔ Bloquant** : validation des maquettes R2 par le référent avant toute ligne de code R2.
**→ Levé le 16/07/2026 : maquettes R2 et les 4 décisions VALIDÉES par le référent** (statuts de demande = extension cohérente, actée). Lancement R2.1 (socle backend exploitation).

---

## 2026-07-16 — Pr. Daaif (+ Claude) — 🏁 R1 CLOSE : recettée, déployée, taguée

**Actions**

- **Recette R1 prononcée par le référent** (écrans ↔ maquette-r1, deux thèmes).
- **Déploiement vérifié en ligne** (<https://siop2.apps.enset.top>) : migration `r1_referentiel` + seed appliqués au boot du conteneur — 5 sites, 8 appareils, écrans /sites servis ; parcours API admin vérifié (locations, assets).
- DoD complète (recette, CI 6 jobs verts, revue pixel, production, journal) → **tag `release/r1`**.

**Prochaine étape** : R2 Exploitation. Maquettes R0 déjà validées pour tableau de bord, liste OT, fiche OT (bilan codé), portail demandeur ; il restera à maquetter le préventif (gabarits, grille du mois, checklist) avant tout code R2 — design d'abord.

---

## 2026-07-16 — Pr. Daaif (+ Claude) — R1.2 : écrans web du référentiel

**Actions**

- **7 écrans fidèles à maquette-r1.html** : Sites (liste + **carte Leaflet/OSM réelle**, pins maquette, création en modale avec position posée au clic), Fiche site (arbre site→zones, appareils, ajout de zone), Ascenseurs (table dense, filtres site/statut, strie rouge sur appareil à l'arrêt), Fiche appareil (écran validé R0 : identité, organes ± ajout/retrait, changement de statut, **QR réel** ; historique R2 et documents R3 annoncés sans simulation), Nouvel ascenseur (identité → rattachement → organes ligne à ligne), **Étiquette A6 imprimable** (`window.print` n'imprime qu'elle, objet papier même en sombre), Personnes & équipes (invitation → **lien d'activation affiché à copier** — pas d'email en R1, décision explicite ; renvoi de lien ; équipes), Catégories (ajout, renommage inline, désactivation).
- **Page /activation** (cible du lien d'invitation, moule visuel de la connexion) — la personne choisit son mot de passe et entre directement.
- **Navigation pilotée par la matrice** : les entrées livrées n'apparaissent que si `canView` (Catégories exige SETTINGS) ; boutons de création/édition conditionnés par `can()` — l'API re-vérifie de toute façon.
- **e2e Playwright** : la **recette R1 officielle rejouée intégralement** (site → zone → appareil+organe → étiquette → invitation → activation du compte → l'invité est connecté) + un parcours « la matrice pilote l'UI » (Technicien : lecture sans boutons) ; purge idempotente des données de test en globalSetup. 5 tests verts, 4 vitest, build prod, lint.

**Leçon**

- Paquet workspace CJS + Vite : les exports nommés runtime échouent (pas de pré-bundle des paquets liés) → alias Vite `@siop/shared` → **source TypeScript** ; l'API garde le dist CJS. Symptôme vicieux : app blanche, tests e2e tous rouges.

**Prochaine étape** : R1.3 — recette R1 avec le référent (revue pixel écrans ↔ maquette-r1), déploiement (CI → Dokploy), tag `release/r1`. Puis R2 Exploitation (maquettes déjà validées pour OT/demandes/portail).

---

## 2026-07-16 — Pr. Daaif (+ Claude) — R1.1 : socle backend du référentiel

**Actions**

- **Modèle R1** (doc + Prisma + migration `r1_referentiel`) : Category (kind EQUIPMENT/COMPONENT_TYPE), Location (site → zone, lat/lng + **colonne PostGIS générée** `geography(Point,4326)` + index GIST), Asset (statut d'équipement), AssetComponent (organe **sans emplacement par construction**), Team (m2m User), invitation sur User (token unique + expiration). Migration **autosuffisante** (`CREATE EXTENSION IF NOT EXISTS postgis`).
- **Contrat** : 21 nouvelles opérations (26 au total) — categories/locations/assets+organes/teams/users/roles/invitations/activate ; générateur OpenAPI étendu aux paramètres de chemin ; spec + client web régénérés dans le même commit.
- **API** : 4 nouveaux modules + gestion des personnes, tous sous `@RequirePermission` (la matrice décide) ; invariants en service : profondeur 2, kinds de catégories, catégorie jamais supprimée, lien d'activation 7 jours à usage unique qui connecte directement.
- **Seed** : parc de la maquette (5 sites + 8 zones, 8 appareils dont B2 à l'arrêt et M1 en maintenance, organes d'A1/B2, 9 catégories, 2 équipes).
- **36 tests verts** (couverture 96 % stmts / 85 % branches) : parcours de recette site→zone→appareil→organes, profondeur 3 refusée, matrice vivante (Technicien lit mais ne crée pas), invitation→activation complète (lien périmé/consommé/renvoyé). Smoke test sur build de prod : sites avec compteurs, fiche A1 et ses 4 organes.
- **CI** : bascule sur `postgis/postgis:18-3.6` (la migration R1 l'exige) — le moment anticipé dans le commentaire du workflow.

**Leçons**

- Migration modifiée après application locale ⇒ réaligner son checksum dans `_prisma_migrations` (ou reset) — d'où la règle : rendre la migration autosuffisante AVANT de l'appliquer.
- Un serveur `reuseExistingServer` de Playwright peut squatter :3000 et faire tester un dist périmé — tuer le port avant tout smoke test.

**Prochaine étape** : R1.2 `apps/web` — écrans Sites (+ carte Leaflet/OSM), Fiche site, Ascenseurs, Nouvel ascenseur, Étiquette QR, Personnes & équipes, Catégories, fidèles à maquette-r1.html.

---

## 2026-07-16 — Pr. Daaif (+ Claude) — R0 CLOSE (tag) · R1 ouverte : maquettes à valider

**Actions**

- **R0 close** : recette prononcée par le référent, tag `release/r0` poussé (DoD complète : CI verte, revue pixel, production en ligne, journal).
- **R1 « Référentiel » ouverte — design d'abord** (principe n°1) : `maquette-r1.html` produite dans le moule validé (CSS répliquée de `maquette-web.html`, mêmes tokens/typo/motifs) — **7 écrans** : Sites (liste + carte PostGIS), Fiche site (hiérarchie d'emplacements), Ascenseurs (liste, statuts d'équipement distincts des statuts OT), Nouvel ascenseur (identité → rattachement → organes), Étiquette QR imprimable (A6 papier, blanche même en thème sombre), Personnes & équipes (invitation par lien d'activation 7 jours, modale montrée), Catégories (référentiels administrables, jamais de suppression si utilisé).
- Rendu vérifié en Chrome headless : 7 écrans + 2 contrôles thème sombre, zéro erreur.

**Décisions (proposées à la validation)**

- Statuts d'équipement (En service / À l'arrêt / En maintenance) **distincts** des statuts OT ; l'appareil à l'arrêt porte la strie rouge.
- Un organe **n'a jamais d'emplacement propre** : il suit son appareil (contrainte en base, comme en v1).
- Invitation par **lien d'activation** (7 jours) — aucun mot de passe créé pour autrui ; compte inactif avant activation.
- Catégorie utilisée : renommage/désactivation seulement, jamais de suppression.

**⛔ Bloquant** : validation des maquettes R1 par le référent avant toute ligne de code applicatif R1.
**→ Levé le 16/07/2026 : maquettes R1 et les 4 décisions de conception VALIDÉES par le référent.** Lancement R1.1 (socle backend).

---

## 2026-07-16 — Pr. Daaif (+ Claude) — 🚀 R0 EN PRODUCTION : <https://siop2.apps.enset.top>

**Actions**

- Blocage de clone résolu (le provider « Custom » HTTPS n'avait pas d'identifiants sur dépôt privé) ; déploiement Dokploy réussi depuis GitHub, domaine posé sur `siop2-web:80`, certificat Let's Encrypt émis.
- **Vérification de l'instance en ligne** (commit `3f9d0d8`) : `/api/health` → `ok` (base, Redis, MinIO `up`) ; 7 comptes démo ; demo-login → `/users/me` (Salma Idrissi, Dispatcher, 10 lignes de matrice) ; `401` sans jeton ; fallback SPA sur `/design` ; HTTPS valide.
- Principe « déployer tôt » honoré : R0 est en ligne avant l'ouverture de R1.

**Reste à faire**

- Poser le secret `DOKPLOY_WEBHOOK_URL` (runbook §4) pour activer le déploiement continu — le job `deploy` saute proprement tant qu'il manque.
- Sauvegardes PostgreSQL côté Dokploy (runbook §5) à configurer.
- **Recette R0 avec le référent** sur l'instance en ligne (revue pixel écrans ↔ maquettes) → clôture du jalon R0 → ouverture R1 Référentiel.

## 2026-07-15 — Pr. Daaif (+ Claude) — R0.13 : Dockerfiles + runbook Dokploy

**Actions**

- **Image API** (multi-stage, node:24-slim) : `pnpm deploy --legacy --prod`, client Prisma régénéré dans l'arborescence déployée, entrypoint `prisma migrate deploy` → seed optionnel (`SEED_ON_START=true`, compilé en `dist/seed`) → API ; utilisateur non-root, HEALTHCHECK `/health`. `prisma` passe en dépendance de production (migrations au boot).
- **Image web** (nginx:alpine) : statique Vite + proxy `/api` → `${API_UPSTREAM}` (template envsubst) — même topologie que le proxy Vite de dev ; cache immuable sur `/assets`, `no-cache` sur `index.html`.
- **`infra/docker-compose.dokploy.yml`** : 5 services préfixés `siop2-`, secrets exigés (`:?`), profils production client / instance démo documentés dans le fichier ; seul `siop2-web` rejoint `dokploy-network` (l'API n'est jamais exposée).
- **Runbook** `docs/06-production/runbook-dokploy.md` : topologie, variables, checklist de première mise en production, releases suivantes, rollback, répétition locale.
- **Répétition locale validée** : images construites, conteneurs lancés sur le réseau infra (migrate + seed au boot), parcours complet vérifié à travers nginx conteneurisé (`/api/health` tout `up`, demo-login → `/users/me`, fallback SPA).

**Leçons de la répétition (consignées pour le playbook)**

- **Prisma en conteneur** : `binaryTargets` explicites dans `schema.prisma` (`debian-openssl-3.0.x` x64 serveur + `linux-arm64-openssl-3.0.x` répétition Mac) — sinon mismatch de moteur au runtime.
- **nginx** : upstream résolu **à la requête** (resolver `127.0.0.11` + variable) et non au boot — sinon nginx refuse de démarrer si l'API n'est pas encore là.
- **Healthcheck alpine** : `127.0.0.1` et non `localhost` (busybox wget tente ::1, nginx écoute en IPv4).
- **Le double verrou ADR-002 a été prouvé en vraie situation** : l'image (NODE_ENV=production) avec `DEMO_MODE=true` sans `DEMO_MODE_I_KNOW` **refuse de démarrer** — comportement observé, pas seulement testé.

**Décisions**

- Migrations **au démarrage du conteneur** (idempotentes) : la base suit toujours le code déployé ; un échec de migration arrête l'API sans servir de trafic.
- Le web est l'unique service exposé (domaine → nginx → proxy interne `/api`) : mêmes chemins en dev et en prod, surface d'attaque minimale.

**Statut** : R0.13 prêt — la première exécution réelle attend les **accès au serveur du partenaire**. Reste pour clore R0 : recette avec le référent (revue pixel écrans ↔ maquettes).

---

## 2026-07-15 — Pr. Daaif (+ Claude) — R0.13 (suite) : instance ENSET + déploiement continu

**Actions**

- **Instance de démonstration décidée** : `https://siop2.apps.enset.top` (Dokploy ENSET, projet compose créé par le référent) — la production client SPELEV suivra la même procédure avec le profil « production client ».
- Job **`deploy`** ajouté au pipeline : appelle le webhook Dokploy sur push `main` **uniquement si lint + contrat + api + web + e2e sont verts** ; « skip » explicite tant que le secret `DOKPLOY_WEBHOOK_URL` n'est pas configuré. L'« Auto Deploy » natif de Dokploy reste désactivé (il ignorerait la CI).
- Runbook §3 réécrit en checklist concrète (source GitHub, Compose Path, variables du profil démo, domaine → `siop2-web:80`) et §4 en procédure CD (secret webhook).

**Prochaine étape** : premier déploiement manuel (runbook §3), pose du secret `DOKPLOY_WEBHOOK_URL` (§4), vérification `https://siop2.apps.enset.top/api/health`, puis recette R0 avec le référent sur l'instance en ligne.

---

## 2026-07-15 — Pr. Daaif (+ Claude) — R0.12 (2/2) : ESLint + parcours Playwright — R0.12 CLOS

**Actions**

- **ESLint 10** (flat config unique à la racine, typescript-eslint, react-hooks sur le web) + scripts `lint` par paquet et job CI dédié. La règle d'architecture ADR-001 est codée : `no-restricted-imports` interdit `minio` partout **sauf** `minio-storage.service.ts` — violation vérifiée par un fichier-test (détectée puis retiré). Monorepo lint : 0 erreur.
- **Playwright** (`apps/web/e2e`) : 3 tests — redirection sans jeton (fermée par défaut), parcours complet connexion démo → coquille (rail actif, chip DÉMO) → **bascule de rôle chronométrée < 3 s** (ADR-002) → /design, et déconnexion avec purge de session. `webServer` démarre l'API construite + Vite ; seed idempotent en globalSetup ; localement `PW_CHANNEL=chrome` (pas de téléchargement).
- CI : jobs `lint` et `e2e` (services PostgreSQL/Redis, artefact playwright-report en cas d'échec). Vitest restreint à `src/` (e2e appartient à Playwright).

**Décisions**

- Une seule config ESLint à la racine (pas une par app) : les règles d'architecture sont transversales et la config est le lieu du cours.

**Prochaine étape** : R0.13 — Dockerfiles (api, web) + runbook Dokploy (`docs/04-*`), puis recette R0 avec le référent (revue pixel écrans ↔ maquettes).

---

## 2026-07-15 — Pr. Daaif (+ Claude) — R0.12 (1/2) : pipeline GitHub Actions

**Actions**

- `.github/workflows/ci.yml`, 3 jobs sur push main + PR : **ci-contract** (régénère `docs/openapi.json` + `schema.d.ts`, échoue au moindre diff — la règle d'or devient bloquante), **api** (PostgreSQL 18 + Redis en services, `prisma migrate deploy`, typecheck, Jest avec **couverture ≥ 70 % bloquante** via `coverageThreshold`), **web** (typecheck + vitest + build prod).
- Test unitaire de `PermissionsGuard` ajouté (aucune route `@RequirePermission` en R0 ne l'exerçait) : couverture 97,5 % stmts / 90,7 % branches, 23 tests.
- Badge CI dans le README ; `ci-contract` simulé en local (diff propre) avant push.

**Décisions**

- CI sur PostgreSQL nu en R0 (la migration `r0_identity` n'exige aucune extension) ; bascule sur l'image `infra/postgres` dès que des tests toucheront pgvector/PostGIS (R1+).
- Pas de MinIO en CI : `/health` répond `degraded` sans casser les tests — seul `database: up` est exigé.

**Prochaine étape** : R0.12 (2/2) — ESLint (dont la règle « pas d'import MinIO hors FileStorage »), parcours Playwright (connexion démo → coquille → bascule de rôle), puis R0.13 Dockerfiles + runbook Dokploy.

---

## 2026-07-15 — Pr. Daaif (+ Claude) — R0.11 : apps/web (connexion + sélecteur démo, coquille, /design)

**Actions**

- `apps/web` (React 19 + Vite + Tailwind v4) : `tokens.css` copié tel quel depuis 02-design ; classes composants **extraites de la maquette validée** ; Manrope auto-hébergée (@fontsource) ; primitives shadcn-style possédées (Button/variants cva, menu Radix).
- **Client typé** : `pnpm generate:client` → `src/api/schema.d.ts` généré depuis `docs/openapi.json` et committé (règle d'or) ; openapi-fetch + injection du jeton.
- Écrans : **connexion** (fidèle maquette, liste démo masquée si l'API répond 404 — ADR-002), **coquille** sidebar (4 groupes, rail safran actif, écrans à venir marqués R1-R3) + topbar (recherche ⌘K, bascule de thème, chip DÉMO, **sélecteur de rôle** dans le menu compte), **/design** (référence vivante : nuanciers, typo, boutons, pastilles), tableau de bord R0 minimal.
- Compléments : tokens `--nav-*` ajoutés à `tokens.css` (valeurs issues de la maquette validée) ; **seed réaligné sur les personas de la maquette** (Salma Idrissi, Ahmed Benali, …) pour des revues pixel cohérentes.
- **Vérifié dans un vrai navigateur** (Playwright + Chrome headless) : parcours démo-login → tableau de bord → bascule Dispatcher→Technicien en **101 ms** (critère < 3 s) → /design clair & sombre ; 7 captures, zéro erreur console. Typecheck, 3 tests vitest, build prod OK.

**Décisions**

- Le web ne lit pas `VITE_DEMO_MODE` : la présence du mode démo est déduite de la réponse de `/auth/demo-accounts` (404 ⇒ rien n'est affiché) — une seule source de vérité, l'API.
- Les entrées de navigation des releases futures restent visibles mais inertes, marquées R1/R2/R3 — le périmètre est annoncé, pas simulé.

**Prochaine étape** : R0.12 — CI GitHub Actions (lint, typecheck, tests, couverture api ≥ 70 %, `ci-contract` sur openapi.json/clients, Playwright du parcours démo) puis R0.13 Dockerfiles + runbook Dokploy.

---

## 2026-07-15 — Pr. Daaif (+ Claude) — R0.10 : apps/api complète (auth, matrice, démo-login, seed)

**Actions**

- `packages/shared` : vocabulaires (7 rôles, 10 catégories d'objets), schémas Zod (auth, profil, santé) et **contrat d'API** ; `pnpm contract` génère `docs/openapi.json` (committée — règle d'or ADR-001).
- `apps/api` (NestJS 11 + Prisma 6) : migration `r0_identity` (Role/Permission/User) ; **guard JWT global fermé par défaut** (+ `@Public()` explicite) ; **PermissionsGuard** (`@RequirePermission`, matrice relue en base, cache 60 s) ; `FileStorage` (seul point d'import MinIO) ; `/health` (base, Redis, stockage).
- **Démo-login ADR-002** : module enregistré uniquement si `DEMO_MODE=true` (sinon routes **404**), double verrou production (`DEMO_MODE_I_KNOW`), refus des comptes `isDemo=false`.
- **Seed idempotent** : 7 rôles, matrice complète (70 lignes), 7 comptes démo (mot de passe commun `SEED_DEMO_PASSWORD` pour la connexion classique).
- **Vérifié bout-en-bout** : 19 tests Jest verts (dont e2e démo on/off) ; smoke test sur build de prod — démo-login → `/users/me` avec matrice, 401 sans jeton, health `ok`.

**Décisions**

- `AppModule.forRoot()` (module dynamique) pour rendre l'enregistrement conditionnel du module démo **testable dans les deux états** — le e2e « routes absentes » est l'exigence n°1 de l'ADR-002.
- Le seed n'écrase jamais une ligne de matrice existante : **la base est la source de vérité des droits**, le fichier n'est que le point de départ.

**Prochaine étape** : R0.11 `apps/web` — login + sélecteur de comptes démo (fidèle à maquette-web.html), coquille sidebar/topbar avec bandeau « DÉMO », page /design, client typé généré depuis `docs/openapi.json`.

---

## 2026-07-15 — Pr. Daaif (+ Claude) — R0 : maquettes VALIDÉES ; architecture + squelette

**Actions**

- **Maquettes HD validées par le référent** → le design est la loi des revues pixel.
- 03-architecture : ADR-001 (stack), ADR-002 (démo-login `DEMO_MODE`, double verrou prod), vue C4, modèle de données R0 (Role/Permission/User, `isDemo`).
- Racine monorepo (pnpm + turbo, Node 24) ; `infra/` : compose local (PostgreSQL 18 pgvector+PostGIS via Dockerfile dédié, Redis, MinIO).

**Décisions**

- **Convention `siop2-`** pour tous services/conteneurs Docker (référent — collisions sur le réseau partagé Dokploy en v1).

**Prochaine étape (reprise)** : R0.10 `apps/api` (auth JWT + matrice permissions + démo-login + seed) → R0.11 `apps/web` (login + sélecteur démo + coquille + /design) → R0.12 CI → R0.13 prépa Dokploy.

---

## 2026-07-15 — Pr. Daaif (+ Claude) — R0 : kickoff, vision, cadrage, design

**Actions**

- **Refondation décidée** : nouveau dépôt `siop-spelev/siop2`, 100 % nouveau code ; la v1 (`siop`) est gelée comme référence. Jalons R0→R5 créés.
- Playbook initialisé : **00-vision** (charte projet, personas, benchmark Atlas), **01-cadrage** (plan de releases avec DoD, user-story map, exigences non fonctionnelles mesurables, registre des risques).
- **02-design** : charte graphique (bleu-treuil / safran / acier, Manrope, motif « rail »), `tokens.css` (source de vérité, bi-thème), palettes analytics **validées par script** (CVD + contraste, modes clair et sombre), **maquettes HD** navigables (7 écrans : connexion + démo-login, tableau de bord, liste OT, fiche OT avec bilan codé, fiche ascenseur + QR, portail demandeur, mobile technicien) — publiées pour revue.

**Décisions (référent, via questions)**

- Diagnostic v1 : esthétique absente, docs éparpillées, périmètre dérivant, bascule de comptes pénible → V2 **design-first**, playbook en livre, releases fermées, **sélecteur de compte démo** (`DEMO_MODE`) dès R0.
- 100 % nouveau code ; périmètre v1 = web + mobile + IA par paliers ; déploiement Dokploy ; nom conservé : **SIOP**.

**Prochaine étape** : validation de la charte + maquettes par le référent (**bloquant** — aucun code applicatif avant), puis 03-architecture + squelette monorepo.

---
