Files
siop2/CLAUDE.md
pr-daaif 4af3f5668a fix(mobile): dictée validée sur iPhone physique — 2 bugs réels corrigés
setAudioModeAsync({allowsRecording:true}) manquant avant recorder.record()
(RecordingDisabledException iOS) ; EXPO_USE_PRECOMPILED_MODULES/
RCT_USE_PREBUILT_RNCORE jamais persistés dans .env (documentés dans ADR-005
mais seulement exportés en shell ad-hoc) — corrigé pour que tout rebuild
depuis zéro ne reproduise plus le crash dyld. Recette terrain complète
vérifiée : dictée → note → clôture → réindexation → recherche sémantique.

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

21 KiB

SIOP V2 — GMAO ascenseurs (refondation design-first)

Contexte

Projet réel (SPELEV, maintenance d'ascenseurs, Maroc) mené par le Pr. Daaif (ENSET) avec ses étudiants. V2 = refondation de siop-spelev/siop (v1, gelée, référence de lecture seulement) décidée le 15 juillet 2026 : 100 % nouveau code, design en premier, documentation en « playbook » pédagogique. Double objectif : produit complet déployé (web + mobile + IA, Dokploy) et playbook du cycle de vie logiciel (cours ENSET + méthode de la future entreprise du référent).

Langue de travail : français (docs, échanges, commits descriptifs). Code et identifiants en anglais.

Principes non négociables (V2)

  1. Design-first : aucune ligne de code applicatif avant validation par le référent de la charte graphique, des tokens et des maquettes HD (docs/02-design/). Chaque release subit une revue « pixel » écrans ↔ maquettes.
  2. Playbook vivant : chaque phase du cycle de vie a son dossier docs/0X-*/ (template réutilisable + artefacts réels). Le journal quotidien vit dans docs/journal/jamais dans le livre.
  3. Périmètre fermé par release : R0 Fondations → R1 Référentiel → R2 Exploitation → R3 Gestion → R4 Mobile → R5 IA. On n'ouvre pas Rn+1 avant recette et déploiement de Rn.
  4. Déployer tôt : chaque release part sur le serveur de production (Dokploy) dès sa recette.
  5. Le développeur est le premier utilisateur : DEMO_MODE=true active un sélecteur de compte démo (connexion 1 clic sur les comptes seedés, endpoint POST /auth/demo-login strictement absent si l'env ne l'active pas). Critère : changer de rôle en < 3 s sans mot de passe.

Stack (ADR-001 — reconduite de v1 car éprouvée, code neuf)

pnpm + Turborepo. apps/api : NestJS, Prisma, PostgreSQL (pgvector + PostGIS), Redis, MinIO derrière l'interface FileStorage (jamais d'import direct du SDK). apps/web : React 19, Vite, TanStack Query/Table, shadcn/ui + Tailwind v4 thématisés par les tokens de docs/02-design/tokens.css, Lucide. apps/mobile : Expo (Expo Go). apps/ai : FastAPI (Python, uv). Règle d'or du contrat : schémas Zod dans packages/shared → OpenAPI committée → clients typés régénérés dans le même commit (CI bloquante).

Conventions

  • Journal de bord : toute session se clôt par une entrée en tête de docs/journal/journal.md (date — auteur — release/tâche ; Actions / Décisions / Prochaine étape).
  • Mode solo temporaire : commits directs sur main tolérés ; à l'arrivée des étudiants → PR + CI obligatoires.
  • Commits conventionnels ; branches courtes feat/…, fix/….
  • Tests écrits avec le code : Jest (api), Vitest (web), jest-expo (mobile), pytest (ai) ; couverture backend ≥ 70 % bloquante ; parcours Playwright par release.
  • API fermée par défaut (guard JWT global, @Public() explicite) ; matrice de permissions en base.
  • Conformité loi 09-08 : géolocalisation limitée au service, purge des audios, consentements.
  • IA : l'IA propose, un humain valide (sauf urgence « personne bloquée ») ; toute réponse RAG cite sa source.

Conventions d'infrastructure

  • Docker/Dokploy : tous les services et conteneurs sont préfixés siop2- (demande du référent, leçon v1 : sur le réseau partagé Dokploy, un service nommé postgres/api collisionne avec les autres projets).

État d'avancement

  • R0 (1/2) : playbook 00-vision, 01-cadrage, 02-design — charte + tokens + maquettes HD VALIDÉES par le référent le 15/07/2026 (artefact : maquette-web.html ; 7 écrans, bi-thème). 03-architecture rédigé (ADR-001 stack, ADR-002 démo-login, C4, modèle R0). Racine monorepo posée (package.json/pnpm-workspace/turbo/.nvmrc) + infra/ (compose PostgreSQL pgvector+PostGIS, Redis, MinIO — services préfixés siop2-).
  • R0.10 apps/api : NestJS + Prisma (migration r0_identity), guard JWT global fermé par défaut + @Public(), PermissionsGuard (matrice en base, cache 60 s), démo-login ADR-002 (module conditionnel, 404 sinon, double verrou prod), seed idempotent (7 rôles, 70 lignes de matrice, 7 comptes démo), FileStorage/health ; packages/shared (Zod) + pnpm contractdocs/openapi.json committée ; 19 tests Jest verts + smoke test build prod.
  • R0.11 apps/web : React 19 + Vite + Tailwind v4, tokens.css copié tel quel + classes extraites de la maquette validée, Manrope auto-hébergée ; connexion + sélecteur démo (masqué si 404 — une seule source de vérité : l'API), coquille sidebar (rail safran, écrans futurs marqués R1-R3) / topbar (thème, chip DÉMO, sélecteur de rôle), page /design ; client typé schema.d.ts généré depuis docs/openapi.json et committé ; vérifié en navigateur réel (bascule de rôle 101 ms, bi-thème, 0 erreur console).
  • R0.12 — tests + CI (5 jobs verts) : lint (ESLint 10 flat config racine, règle ADR-001 anti-import MinIO codée et vérifiée), ci-contract (régénération spec+client, diff bloquant), api (PostgreSQL 18 + Redis, migrate deploy, Jest couverture ≥ 70 % bloquante — mesurée 97,5 %), web (typecheck + vitest + build), e2e (Playwright : parcours démo, bascule < 3 s chronométrée, déconnexion). Badge au README.
  • R0.13 — Dockerfiles + runbook Dokploy : image api (multi-stage pnpm deploy, prisma migrate deploy au boot, seed optionnel SEED_ON_START, non-root, healthcheck ; binaryTargets explicites) ; image web (nginx, proxy /api résolu à la requête, fallback SPA, cache assets) ; infra/docker-compose.dokploy.yml (5 services siop2-, seul le web sur dokploy-network) ; runbook docs/06-production/runbook-dokploy.md. Répétition locale complète validée (migrate+seed au boot, parcours via nginx conteneurisé, double verrou ADR-002 observé). Le déploiement réel attend les accès au serveur du partenaire.
  • R0 CLOSE (tag release/r0) — en production : https://siop2.apps.enset.top (Dokploy ENSET, profil démo, vérifiée en ligne). Restes non bloquants : secret DOKPLOY_WEBHOOK_URL (CD), sauvegardes PostgreSQL Dokploy. Production client SPELEV : attend les accès au serveur du partenaire.
  • R1 — maquettes validées (16/07) : maquette-r1.html, 7 écrans + 4 décisions (statuts d'équipement ≠ statuts OT ; organe sans emplacement par construction ; invitation par lien 7 j ; catégories jamais supprimées si utilisées).
  • R1.1 — socle backend : migration r1_referentiel (Category, Location site→zone + PostGIS générée, Asset, AssetComponent, Team, invitation User), 21 nouvelles opérations au contrat (26 total), 4 modules API + invitations/activation sous @RequirePermission, seed parc maquette (5 sites, 8 appareils, organes, 2 équipes), 36 tests (96 %/85 %), CI sur postgis/postgis:18-3.6.
  • R1.2 — écrans web du référentiel : 7 écrans fidèles à maquette-r1.html (Sites + carte Leaflet/OSM, fiche site, ascenseurs, fiche appareil + QR réel, création, étiquette A6 imprimable, personnes & équipes avec lien d'activation à copier, catégories) + page /activation ; navigation et actions pilotées par la matrice (usePermissions) ; recette R1 rejouée en e2e Playwright (dont activation d'un invité) ; alias Vite @siop/shared → source TS (leçon CJS/workspace).
  • 🏁 R1 CLOSE (16/07/2026, tag release/r1) : recettée par le référent, déployée et vérifiée en ligne (migration + seed au boot, 5 sites / 8 appareils sur l'instance).
  • R2 — maquettes validées (16/07) : maquette-r2.html (demandes/approbation, nouvel OT, préventif, checklist, compteurs) + 4 décisions actées.
  • R2.1 — socle backend exploitation : migration r2_exploitation (10 tables), 15 opérations au contrat (41 total, transitions + champs requis du bilan dans @siop/shared), machine à états stricte avec garde de clôture (bilan 3 champs + checklist), demande→OT 1-1, rejet à motif, scoping « voir autre » (listes + accès directs), seed maquette (31 valeurs de bilan, 8 gabarits, OT/demandes/compteurs), 45 tests (95 %/79 %).
  • R2.2 — préventif + compteurs : underContract + periodKey (unicité [assetId, periodKey] = idempotence EN BASE), génération mensuelle (ancrage mise en service, premier contrôle, gabarits administrables, statut du mois), compteurs strictement croissants ; +7 opérations (48), 50 tests (94,7 %/78 %). Automatisation cron/BullMQ notée pour le durcissement production.
  • R2.3 — écrans web exploitation : liste/fiche OT (transitions via allowedTransitions, garde visible, bilan codé, checklist cliquable, activité), demandes+approbation/rejet motivé, nouvel OT (interrupteur urgence), préventif (tuiles+générer+gabarits), compteurs, tableau de bord réel, urgence traversante (chip topbar, badges, bandeau) ; GET /assets/options (trou Demandeur corrigé) ; retry sur collision de référence dans la génération ; 9 Playwright verts (recette R2 complète), 50 tests API.
  • 🏁 R2 CLOSE (16/07/2026, tag release/r2) : recettée (1 anomalie corrigée en recette : tri « Interventions récentes »), déployée et vérifiée en ligne (portail /q/A1, urgence en tête, préventif de juillet généré).
  • R3 — maquettes validées (16/07) + R3.1 socle backend gestion : migration r3_gestion (Partner, Part sans colonne de quantité, StockMovement signé/tracé/PU figé, PurchaseOrder, LaborTime taux figé, Document, User.hourlyRate) ; 66 opérations ; stock = Σ mouvements (jamais négatif, en transaction), réception BC → RECEIPT + lastUnitPrice, conso/MO à prix/taux FIGÉS, WorkOrderDetail.costs immuable après clôture ; seed maquette (OT-0341 = 505 MAD, testé) ; 55 tests (92 %/74,9 %). Durcissement : références par séquences Postgres (fin des courses max+1).
  • R3.2 — bibliothèque + analytics : FileStorage complet (put/stream/remove, bucket auto), upload multipart typé (20 Mo, rattachement requis, permission sur la cible), download streamé par l'API, octets vérifiés à l'identique en e2e ; GET /analytics/summary dérivé du réel (pannes par organe via bilans codés, coûts figés, taux préventif, top équipements) ; MinIO en CI ; 71 opérations, 58 tests (6 runs consécutifs verts).
  • R3.3 : 7 écrans web fidèles à maquette-r3 (stock/alertes, fiche pièce, BC + réception, création BC préremplie depuis l'alerte, tiers, bibliothèque, statistiques) + fiche OT complète (coûts réels : consommer à prix figé / saisir temps à taux figé ; carte Documents) + fiche ascenseur (Documents) + taux horaire dans Personnes + nav Ressources/Statistiques activée par la matrice. Recette R3 automatisée (13/13 e2e Playwright).
  • R3.4 — corrections de recette + recherche globale (17/07) : revue pixel (artefact) → 8 écarts corrigés sur arbitrage du référent (filtre fournisseur + tri sous-seuil + entrée de stock en liste, fournisseur cliquable, sélecteur de période 3/6/12 sur les stats, rattachements syndic→site via Location.partnerId (migration r3_recette_fixes), filtre « Rattaché à » + glisser-déposer en bibliothèque, aperçus/ouverture des documents) ; recherche globale ⌘K (GET /search, 73 opérations, familles filtrées par la matrice, « voir autre » respecté). 74 tests API, 14/14 Playwright.
  • 🏁 R3 CLOSE (17/07/2026, tag release/r3) : recettée par le référent (revue pixel + 8 corrections + recherche globale, CI verte). Reste : redéploiement Dokploy (manuel, webhook CD absent) et vérification en ligne.
  • R4 — maquettes rédigées (17/07) : maquette-r4.html, 7 écrans mobile technicien offline-first (Ma journée bi-état, fiche OT, clôture terrain avec garde, scan QR des étiquettes R1, fiche ascenseur, checklist en file, synchro & conflits à verrou optimiste) + 5 décisions à acter (offline-first en file, verrou optimiste tranché par l'humain, périmètre fermé technicien, scan local, photos en file — ni audio ni géoloc en R4).
  • R4 — maquettes + décisions D1-D5 VALIDÉES par le référent (17/07) ; R4.1 socle mobile : apps/mobile (Expo SDK 57, TS strict), connexion + sélecteur démo ADR-002, tabbar (onglets futurs marqués), « Ma journée » triée priorité/échéance, cache TanStack persisté (lecture hors-ligne D1), jeton SecureStore, client typé du contrat, CORS_ORIGINS opt-in côté API (Expo web/debug seulement), 6 tests jest-expo + job CI mobile. Vérifié 10/10 en Expo web (connexion démo → Ma journée → hors-ligne/retour).
  • R4.2 — terrain en ligne : scanner QR réel (expo-camera, analyseScan testée, résolution locale D4, QR étrangers refusés), fiche ascenseur (D3, historique scopé), fiche OT (machine à états, coûts figés, garde par closureBlockers API), clôture terrain (bilan 6 champs au pouce), préventif → grille cochable (appui long = N/A, aria-checked). Écritures en ligne assumées (bandeaux) — la file est R4.3. Vérifié 12/12 en Expo web (dont clôture complète d'un OT de test), 12 tests jest-expo.
  • R4.3 — file & verrou : verrou optimiste serveur (baseUpdatedAt → 409 contextualisé, toute écriture avance la version, web inchangé, ADR-003 sécurité/routage mobile) ; file persistée rejouée dans l'ordre (propagation de version intra-lot, arrêt sur conflit, patchs optimistes, photos D5 compressées en file), écran Synchro & conflits (rejouer sur version à jour / abandonner), préchargement parc+référentiels, purge complète à la déconnexion. Recette « mode avion » 13/13 en Expo web (conflit tranché par l'humain, serveur DONE + bilan), 17 tests jest-expo, 74 tests API.
  • 🏁 R4 CLOSE (17/07/2026, tag release/r4) : posé sur décision du référent avec la recette « mode avion » validée 13/13 en Expo web piloté ; la recette sur téléphone (Expo Go — vrai mode avion, scan caméra) est reportée et reste due avant toute production client mobile.
  • R5 — maquettes rédigées (17/07) : maquette-r5.html, 6 écrans (assistant RAG à citations sources document/page/extrait, refus explicite hors corpus, suggestion de bilan web+mobile à validation humaine, corpus & ingestion administrable, voix opt-in avec purge) + 5 décisions à acter (aucune écriture automatique, sourcé ou silencieux, corpus fermé, anonymisation 09-08, voix purgée).
  • R5 — maquettes + décisions D1-D5 VALIDÉES par le référent (17/07) ; R5.1 socle apps/ai : ADR-004 (embeddings locaux fastembed 384d, pgvector via migration Prisma r5_ia (RagChunk + corpus sur Document), génération opt-in — mode extractif par défaut, service jamais exposé joint par l'API seule), pipeline d'ingestion anonymisé D4 (fonction pure testée, PDF paginés + bilans codés), /internal/reindex + /internal/search sous jeton de service, 14 pytest + ruff + job CI ai (embeddeur déterministe en CI). Vérifié en réel : corpus seedé indexé en 7 s, recherche sémantique concluante, 0 identité dans les chunks.
  • R5.1+ génération opt-in : Generateur ADR-004 §3 — extractif par défaut, AI_GENERATION=api + AI_API_KEY (exigée au boot, jamais loguée ; Dokploy secrets) + AI_MODEL (défaut claude-opus-4-8), SDK anthropic en extra optionnel, citations [n] obligatoires, repli extractif sur tout échec (dont stop_reason=refusal). R5.2 assistant au contrat : POST /assistant/ask (WORK_ORDERS.view) + POST /assistant/suggest-bilan (WORK_ORDERS.edit) proxifiés par NestJS (traduction dialecte interne → contrat, 503 propre), suggestion = codes EXISTANTS seulement (confiance + « N bilans similaires »), normalisation fastembed corrigée (débusquée en chaîne réelle), 23 pytest, tests API sur stub HTTP.
  • R5.3 — écrans IA : page web Assistant (chat sourcé, refus honnête chiffré, « Ouvrir » vers PDF/OT), Bibliothèque = corpus administrable (statut d'indexation par document, interrupteur d'exclusion, « Réindexer tout », bandeau 09-08), suggestions fiche OT (« Appliquer » = geste humain, liseré « suggéré » retiré au choix manuel) et clôture mobile (chips, « réseau requis » hors-ligne) ; contrat 76 opérations (corpus sur Document, PATCH /documents/{id}/corpus, POST /assistant/reindex — ci-contract vérifie aussi le client mobile) ; seuils AI_SEUIL_* par env ; job e2e CI avec siop2-ai (embeddeur déterministe, seuils calibrés sur mesures réelles), 16/16 Playwright, 78 tests API ; chaîne vérifiée au vrai modèle ONNX (web 7/7, mobile Expo web 6/6, 0 erreur console).
  • Recette R5 sans clé API + durcissement (17/07) : la recette a invalidé MiniLM-384 (page-réponse classée derrière des passages sans rapport, 0,24 vs 0,41) → bascule mesurée vers paraphrase-multilingual-mpnet-base-v2 768 d (ADR-004 amendé, migration r5_embeddings_mpnet, découpage ~350 car., seuils 0,45/0,40/0,55) ; recette type ✓ (réponse sourcée p. 2, refus honnête, D1-D5, tout en extractif) ; revue pixel publiée (artefact, 3 arbitrages) ; Dockerfile siop2-ai (modèle au build, non-root), compose Dokploy (service interne, AI_SERVICE_TOKEN requis, génération opt-in), runbook §5-6 (service IA, calibrage seuils client, réindexation post-déploiement).
  • Revue pixel R5 validée par le référent (17/07) — 1 correction appliquée sur arbitrage : la Bibliothèque-corpus passe en tableau (Document/Rattaché à/Indexation/Corpus + actions ; interrupteur éteint pour les non-indexables), vignettes conservées sur les cartes Documents des fiches ; « Appliquer » direct et calibrage au runbook validés tels quels. 16/16 Playwright rejoués.
  • 🏁 R5 CLOSE (17/07/2026, tag release/r5) : recettée par le référent (revue pixel + arbitrage tableau), recette passée SANS clé API, durcissement répété en local conteneurisé. R0 → R5 : périmètre v1 couvert.
  • Recette terrain mobile sur iPhone 15 Pro — 7/7 validés (19/07, ADR-005) : Expo Go bloqué par le retard d'approbation Apple (SDK 54 vs notre SDK 57) → décision : builds natifs locaux (Xcode/Android Studio, signature gratuite) pour la vraie recette, Expo Go conservé pour l'aperçu sans installation. 5 obstacles techniques réels corrigés et documentés (UDID plutôt que nom, ne jamais contourner expo run:* par un xcodebuild manuel, modules Expo/RN précompilés SDK 56/57 incompatibles avec la liaison statique du projet, fetch global incompatible avec l'upload multipart natif — fix permanent EXPO_PUBLIC_USE_RN_FETCH=1 dans apps/mobile/.env, découverte réseau du dev client peu fiable). Recette iOS : connexion, scan caméra réel (résolution + rejet QR étranger), vrai mode avion, conflit D2 tranché par l'humain avec photo réellement téléversée, persistance à travers fermeture/reconstruction de l'app, suggestions R5 au vrai modèle, purge de sécurité au changement de compte — tout validé. Android : build natif sur émulateur, passage santé complet.
  • Dictée implémentée (22/07, ADR-004 §5) : écran Voix R5 (jamais construit) — faster-whisper local (CTranslate2, CPU, opt-in AI_TRANSCRIPTION=off|locale, défaut off), audio jamais persisté (purge serveur ET mobile quoi qu'il arrive), transcription → relecture humaine → suggestion immédiate et/ou sauvegarde dans InterventionReport.note (champ existant depuis R2, jamais eu d'écran jusqu'ici) → note rejoint le corpus à la clôture comme les bilans codés. Mobile : expo-audio/expo-file-system, bouton dicter sur l'écran de clôture. Docker siop2-ai embarque le modèle (1,54→2,19 Go). Vérifié réellement (voix de synthèse → texte fidèle, bout en bout API, conteneur Docker construit, chaîne corpus complète note→clôture→réindexation→recherche). 80 tests API, 29 pytest ai, mobile vert.
  • Dictée validée sur iPhone physique (01/08, ADR-004 §5) : le blocage USB du 22/07 était un faux négatif de system_profiler (outil de diagnostic en panne, pas le câble). Deux bugs réels trouvés et corrigés : expo-audio iOS exige setAudioModeAsync({ allowsRecording: true }) avant record() (absent, jamais détecté hors appareil réel) ; le correctif des modules précompilés (ADR-005) n'avait jamais été rendu permanent dans apps/mobile/.env — corrigé. Chaîne complète vérifiée en vrai : micro → dictée → transcription fidèle → note → clôture → réindexation → retrouvé par la recherche sémantique.
  • 🔄 Reprise ici : redéploiement Dokploy de l'instance ENSET (AI_SERVICE_TOKEN à créer — runbook §2 — puis « Réindexer tout »). Restes : recette Android sur appareil physique, calibrage AI_SEUIL_* et qualité darija sur corpus SPELEV réel, secret DOKPLOY_WEBHOOK_URL, production client SPELEV (attend les accès serveur du partenaire).
  • Détail quotidien : docs/journal/journal.md. Dépôt : siop-spelev/siop2 (privé), jalons R0→R5.