# 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) — R6.6 : Demandeur restreint à son site **Contexte** — en recette, le référent a remarqué que Karim Doukkali (Demandeur) voit tout le parc dans le sélecteur d'équipement de « Nouvelle demande » : il pourrait accidentellement signaler une panne sur un ascenseur qu'il ne gère pas. Investigation : le trou est dans l'**API** — `GET /assets/options` (`assets.service.ts`) n'avait aucun filtre, et cet endpoint est utilisé aussi bien par le web que par le mobile. Décision retenue : les deux à la fois — association Demandeur → site (corrige web ET mobile) et scan QR comme raccourci mobile. **Actions** - Relation many-to-many `User ↔ Location` (`assignedSites`/`assignedUsers`, migration `r6_demandeur_sites`, même style que `Team.members`) — vide = aucune restriction (comportement historique, inchangé pour tous les rôles sauf un Demandeur affecté). - Contrat (`packages/shared/schemas/users-admin.ts`) : `InvitationCreate`/`UserUpdate` gagnent `locationIds?` (remplace l'affectation, comme `teamIds`) ; `UserAdmin` gagne `assignedSites`. - `AssetsService.allowedLocationIds(user)` : site(s) + zones filles affectés, ou `null` si aucune restriction — réutilisée par `options()` **et** par `RequestsService.create` (défense en profondeur : un `assetId` soumis directement hors périmètre est rejeté, 400) pour que les deux filtres ne divergent jamais. - `UsersService` : `assertTopLevelSites` (un Demandeur est affecté à un site, jamais une zone — même invariant que la hiérarchie site/zone) ; `invite()`/`update()` branchent `locationIds`. - Web (`personnes.tsx`) : `ModaleInvitation` affiche une liste de sites à cocher quand le rôle choisi est Demandeur ; colonne « Sites » dans le tableau, éditable via une petite modale dédiée (`ModaleSites`, même hook `useUpdateUser` que le taux horaire). - Mobile (`formulaire-demande.tsx`) : bouton « 📷 Scanner l'étiquette » sous le sélecteur d'équipement — réutilise `analyseScan`/`expo-camera` du Scanner R4, mais résout **uniquement** contre les options déjà chargées (déjà filtrées par site) — jamais de repli sur le parc complet, ce qui annulerait la restriction. Aucun changement à `useAssetOptions()` : le filtrage serveur profite automatiquement au formulaire. - Seed : Karim Doukkali rattaché à **Tour Atlas** (cohérent avec le `guardianName` déjà présent dans le seed pour ce site) ; ses demandes historiques sur d'autres sites restent visibles en lecture, seule la création est désormais restreinte. - **Bug trouvé en vérification (pas en recette, avant tout commit)** : la première version de `RequestsService.create` comparait `allowed.includes(dto.assetId)` — mais `allowed` est une liste d'**ids de sites/zones**, pas d'ids d'appareils. Comparaison apples-to-oranges, aurait rejeté TOUT signalement d'un Demandeur affecté, y compris dans son propre périmètre (repéré en testant B2/Tour Atlas avec Karim, qui échouait à tort). Corrigé : comparaison sur `asset.locationId`. Méthode renommée `allowedAssetIds` → `allowedLocationIds` pour que le nom dise ce qu'elle retourne réellement. - Vérifié : filtrage confirmé côté serveur (Dispatcher = 8 appareils inchangés, Karim = 4 appareils de Tour Atlas seulement) ; défense en profondeur confirmée (asset hors périmètre → 400, asset dans le périmètre → 201). Suite de tests API : 79/80 verts — le seul échec (`documents-analytics.e2e-spec.ts`, `monthCost`) est le flake déjà identifié cette session (fenêtre calendaire réelle vs données de seed), sans rapport avec R6.6 ; `exploitation.e2e-spec.ts` mis à jour (A1/C1 → A2/B1, dans le site de Karim, sinon rejetés par la nouvelle règle — c'est le comportement voulu). Typecheck/tests/lint verts sur les 4 paquets. **Décisions** - Affectation au niveau **site** (pas zone, pas appareil individuel) — même granularité que `Partner.sites`, cohérent avec le modèle existant. - Gestion des sites d'un Demandeur reste une action **web uniquement** (admin) — le mobile n'a que le raccourci scan, aucune UI de gestion. **Prochaine étape** : reste de la recette R6 (Sites/Ascenseurs/Fichiers côté Gestionnaire, puis passage Demandeur avec le nouveau périmètre) ; `expo-sharing`/`expo-clipboard` en réserve ; restes non bloquants inchangés (Dokploy, Android physique, `AI_SEUIL_*`/darija, `DOKPLOY_WEBHOOK_URL`). --- ## 2026-08-02 — Pr. Daaif (+ Claude) — Assistant mobile : dictée confirmée sur iPhone **Actions** - Suite directe de la recette Gestionnaire : trois allers-retours sur l'écran Assistant. 1. Crash au premier essai (`expo-file-system` deleteAsync déprécié, SDK 57) — corrigé par l'import du sous-chemin `expo-file-system/legacy` (même fonction, comportement inchangé), appliqué aussi à la clôture (`cloture.tsx`) qui utilisait le même appel sans avoir été re-exercée depuis. 2. Texte transcrit pas entièrement visible pour la relecture (D5) — la barre de saisie sur une ligne a été remplacée par une zone multiligne pleine largeur, « Envoyer » en geste séparé. 3. Le bouton « Envoyer » chevauchait encore le texte — deux causes cumulées : le `ScrollView` du chat sans `style={flex:1}` (mal borné dans son parent), et le champ multiligne qui s'appuyait sur `maxHeight` seul (ignoré par iOS dans ce cas, le champ grandissait avec le contenu). Hauteur fixe + défilement interne garanti. - **Confirmé par le référent sur iPhone physique : la dictée de la question dans l'Assistant fonctionne** (relecture complète, envoi, réponse sourcée). **Décisions** - Aucune — trois corrections successives sur un même problème remonté en conditions réelles, aucune n'aurait été trouvée par typecheck/tests/lint seuls (aucun test n'exerce le rendu RN réel ni le comportement spécifique iOS de `TextInput`/`ScrollView`). **Prochaine étape** : reste de la recette Gestionnaire (Sites/Ascenseurs/Fichiers à confirmer explicitement), puis passage Demandeur. --- ## 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 ` 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 `