- Contrat (76 opérations) : Document expose inCorpus/indexedAt/chunkCount,
PATCH /documents/{id}/corpus (ASSETS.edit), POST /assistant/reindex
(bilan chiffré) ; ci-contract vérifie désormais aussi le client mobile.
- Web : page /assistant (chat sourcé — extraits exacts cités, Ouvrir vers
PDF authentifié ou fiche OT, avertissement permanent ; refus honnête
chiffré avec action utile) ; Bibliothèque = corpus (bandeau 09-08,
statut d'indexation par document, interrupteur d'exclusion PDF,
Réindexer tout) ; fiche OT : « Décrire pour suggérer » (Appliquer =
geste humain, liseré « suggéré » retiré au choix manuel).
- Mobile : chips de suggestion dans la clôture (un appui = un champ
pré-rempli, « réseau requis » hors-ligne — la file R4 n'en dépend pas).
- apps/ai : seuils AI_SEUIL_* configurables par env (CI + calibrage).
- CI e2e : service siop2-ai (embeddeur déterministe, seuils calibrés sur
mesures : match 0,66 vs bruit 0,11) + parcours R5 Playwright (PDF généré
xref valide → réindexation → réponse sourcée → refus → suggestion).
- Vérifié : 16/16 Playwright, 78 tests API, 23 pytest, 17 jest-expo ;
chaîne réelle au vrai modèle ONNX (web 7/7, mobile Expo web 6/6).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
65 KiB
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-07-17 — Pr. Daaif (+ Claude) — R5.3 : les écrans de l'IA (web + mobile) et le corpus administrable
Actions
- Contrat (76 opérations) :
Documentexpose 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 leGenerateur(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 verssiop2-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'interfaceGenerateurd'ADR-004 §3 prend corps —GenerateurExtractif(contrat de base, aucun LLM) etGenerateurAPI(SDK officielanthropic, 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éfautclaude-opus-4-8)..env.exampleposé ;/healthzexpose 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é,
GenerateurAPIconstruit 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_SECRETet 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 (tableRagChunkpossédée par Prisma, migrationr5_ia+ état de corpus sur Document), génération opt-in (AI_GENERATION=offpar défaut : mode extractif honnête, la recette doit passer sans clé API), suggestion de bilan sans LLM (similarité sémantique, explicable). Topologie :siop2-aijamais exposé, joint par l'API NestJS seule (X-Service-Token). apps/aiposé (FastAPI + uv, python 3.11) : config validée au démarrage,/healthz,/internal/reindex(idempotent) et/internal/searchproté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 →RagChunkavec 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/r4posé et poussé (CI verte surc8b3c17: 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) :
updatedAtexposé au détail OT ;baseUpdatedAtoptionnel 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/REFde 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
closureBlockersde 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.checkedne produit PASaria-checked— utiliser la proparia-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/mobileposé (Expo SDK 57, TypeScript strict, pnpm workspace,@siop/sharedconsommé) : 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é depuisdocs/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
mobileajouté (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.changeobservé). - Leçon Playwright :
setOfflinene déclenche pas les événements réseau du navigateur — NetInfo web écoutenavigator.connectionsur 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/r3posé 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 (
monthsau contrat, libellés et barres suivent). Tiers : les syndics affichent leurs sites — nouveau lien site→client/syndic (migrationr3_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 permissionviewdu 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 ongletblob:(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-e2epurge 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
FileStoragegagne 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 à jourlastUnitPrice), 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/A1opé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 parréférence + jeton(pas de jeton, pas de lecture — aucune énumération possible). - Suivi sans compte :
Request.publicToken(migrationr2_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-idne 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) etWorkOrder.periodKeyavec 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 exposeallowedTransitionsetclosureBlockers(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
POSTde 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.htmlcomplè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.printn'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 parcan()— 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éegeography(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
reuseExistingServerde 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/r0poussé (DoD complète : CI verte, revue pixel, production en ligne, journal). - R1 « Référentiel » ouverte — design d'abord (principe n°1) :
maquette-r1.htmlproduite dans le moule validé (CSS répliquée demaquette-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, MinIOup) ; 7 comptes démo ; demo-login →/users/me(Salma Idrissi, Dispatcher, 10 lignes de matrice) ;401sans 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 jobdeploysaute 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, entrypointprisma migrate deploy→ seed optionnel (SEED_ON_START=true, compilé endist/seed) → API ; utilisateur non-root, HEALTHCHECK/health.prismapasse 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-cachesurindex.html. infra/docker-compose.dokploy.yml: 5 services préfixéssiop2-, secrets exigés (:?), profils production client / instance démo documentés dans le fichier ; seulsiop2-webrejointdokploy-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/healthtoutup, demo-login →/users/me, fallback SPA).
Leçons de la répétition (consignées pour le playbook)
- Prisma en conteneur :
binaryTargetsexplicites dansschema.prisma(debian-openssl-3.0.xx64 serveur +linux-arm64-openssl-3.0.xré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.1et nonlocalhost(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=truesansDEMO_MODE_I_KNOWrefuse 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
deployajouté au pipeline : appelle le webhook Dokploy sur pushmainuniquement si lint + contrat + api + web + e2e sont verts ; « skip » explicite tant que le secretDOKPLOY_WEBHOOK_URLn'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
lintpar paquet et job CI dédié. La règle d'architecture ADR-001 est codée :no-restricted-importsinterditminiopartout saufminio-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.webServerdémarre l'API construite + Vite ; seed idempotent en globalSetup ; localementPW_CHANNEL=chrome(pas de téléchargement). - CI : jobs
lintete2e(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èredocs/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 viacoverageThreshold), web (typecheck + vitest + build prod).- Test unitaire de
PermissionsGuardajouté (aucune route@RequirePermissionen R0 ne l'exerçait) : couverture 97,5 % stmts / 90,7 % branches, 23 tests. - Badge CI dans le README ;
ci-contractsimulé en local (diff propre) avant push.
Décisions
- CI sur PostgreSQL nu en R0 (la migration
r0_identityn'exige aucune extension) ; bascule sur l'imageinfra/postgresdès que des tests toucheront pgvector/PostGIS (R1+). - Pas de MinIO en CI :
/healthréponddegradedsans casser les tests — seuldatabase: upest 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.csscopié 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.tsgénéré depuisdocs/openapi.jsonet 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 contractgénèredocs/openapi.json(committée — règle d'or ADR-001).apps/api(NestJS 11 + Prisma 6) : migrationr0_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 comptesisDemo=false. - Seed idempotent : 7 rôles, matrice complète (70 lignes), 7 comptes démo (mot de passe commun
SEED_DEMO_PASSWORDpour 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/meavec matrice, 401 sans jeton, healthok.
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.