Files
siop2/docs/journal/journal.md
pr-daaif 39320d081e fix(mobile): R6.8 — la connexion routait toujours vers "Ma journée"
connexion.tsx faisait router.replace('/(tabs)/journee') en dur — trouvé
en recette : Karim (Demandeur) atterrissait sur l'écran du Technicien
après avoir basculé de compte démo. La redirection par rôle
(ongletAccueil) n'était branchée qu'à l'aiguillage initial (app/index.tsx,
R6.1), pas au retour de connexion — le chemin réellement emprunté à
chaque bascule de compte (pas de sélecteur de rôle en direct sur mobile,
changer de compte = se déconnecter puis se reconnecter).

useLogin()/useDemoLogin() (auth/session.tsx) renvoient maintenant la
réponse complète (contient user.role.name) au lieu de la jeter après en
avoir extrait le jeton. connexion.tsx : entrer(role) route vers
/(tabs)/${ongletAccueil(role)} pour les deux chemins (connexion classique
et sélecteur démo).

Au passage : sous-titre "Technicien" et accroche "sur le terrain — même
sans réseau" (reste de R4, mobile alors réservé au Technicien) devenus
trompeurs pour les autres rôles depuis R6 — généricisés.

Typecheck/tests/lint verts sur les 4 paquets.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 09:37:12 +01:00

1302 lines
110 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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-03 — Pr. Daaif (+ Claude) — R6.8 : connexion routait toujours vers « Ma journée »
**Contexte** — en recette, le référent a basculé sur Karim Doukkali (Demandeur) et a atterri sur
« Ma journée » (l'écran du Technicien, « Aucun OT en cours ») au lieu d'« Accueil ». Cause :
`app/connexion.tsx` — la fonction `entrer()` faisait `router.replace('/(tabs)/journee')` **en
dur**, quel que soit le rôle du compte choisi. La redirection par rôle (`ongletAccueil`) n'avait
été branchée qu'à l'aiguillage initial (`app/index.tsx`, R6.1) — pas au retour de connexion, le
chemin réellement emprunté à chaque bascule de compte démo (changer de compte = se déconnecter
puis se reconnecter, il n'y a pas de sélecteur de rôle en direct sur mobile).
**Actions**
- `useLogin()`/`useDemoLogin()` (`auth/session.tsx`) renvoient maintenant la réponse complète
(contient `user.role.name`) au lieu de la jeter après en avoir extrait le jeton.
- `connexion.tsx` : `entrer(role)` route vers `/(tabs)/${ongletAccueil(role)}` — les deux
boutons (connexion classique et sélecteur démo) passent le rôle réellement retourné par l'API,
jamais un onglet figé.
- Au passage : le sous-titre « Technicien » et l'accroche « sur le terrain — même sans réseau »
de l'écran de connexion étaient un reste de R4 (mobile alors réservé au Technicien) — devenus
trompeurs pour les autres rôles depuis R6, généricisés.
- Typecheck/tests/lint verts sur les 4 paquets.
**Décisions**
- Aucune — correction directe, aucun changement de comportement voulu au-delà du routage.
**Prochaine étape** : reprendre la recette Demandeur (Accueil, Nouvelle demande filtrée à Tour
Atlas, scan QR) une fois ce correctif rechargé sur l'iPhone.
---
## 2026-08-02 — Pr. Daaif (+ Claude) — R6.7 : assignation à l'approbation (mobile)
**Contexte** — en recette, le référent a signalé que le Gestionnaire doit pouvoir affecter un OT
à un technicien au moment où il approuve une demande — « ce n'est pas à lui d'agir comme un
technicien ». Vérification : le web le fait déjà (`PanneauApprobation`, `demandes.tsx`
sélecteur « Assigner à » parmi les techniciens actifs). Le mobile (`PanneauDemandes`, construit
en R6.1) approuvait directement avec la priorité seule, sans écran d'assignation — l'OT partait
non assigné, obligeant quelqu'un à revenir dessus ensuite pour le confier à un technicien.
**Actions**
- `apps/mobile/src/composants/panneau-demandes.tsx` : le bouton « Approuver → OT » ouvre
désormais un panneau (même patron que le rejet à motif) avec un `ChoixTel` « Assigner à »,
peuplé des techniciens actifs (`useUsers()` de `api/pilotage.ts`, filtre
`status === 'active' && role.name.startsWith('Technicien')` — identique au web), avant de
confirmer l'approbation (`assigneeIds` transmis à `POST /requests/{id}/approve`, déjà supporté
côté API et par le contrat — rien à changer côté serveur, seule l'UI mobile manquait cette
étape).
- Vérifié de bout en bout : demande créée par Karim → approuvée par Nadia (Gestionnaire) avec
Ahmed (Technicien) assigné → OT confirmé avec `assignees: [Ahmed Benali]`. Typecheck/tests/lint
verts sur les 4 paquets.
**Décisions**
- Le périmètre plus large — le Gestionnaire garde par la matrice R2/R3 `WORK_ORDERS` en accès
complet (`view/viewOther/create/edit/delete`), ce qui lui permet TECHNIQUEMENT de démarrer/
clôturer n'importe quel OT comme le ferait un technicien — n'est pas touché ici : c'est un
choix de matrice établi depuis R2/R3, identique sur le web, pas une régression du mobile R6.
Signalé au référent comme question ouverte séparée si un resserrement est souhaité.
**Prochaine étape** : reste de la recette R6 (Sites/Ascenseurs/Fichiers, puis passage Demandeur
avec le nouveau périmètre de site et le scan QR) ; question ouverte sur le périmètre d'édition OT
du Gestionnaire à trancher si besoin.
---
## 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 <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.
---