Files
siop2/CLAUDE.md
pr-daaif 59ed6f3952 feat(r5): dictée — audio local (faster-whisper) → note → corpus (ADR-004 §5)
Écran Voix R5 (maquetté, jamais construit) implémenté sur décision du
référent : open-source et local, pas d'API externe.

- apps/ai : faster-whisper (CTranslate2, CPU, MIT) opt-in
  (AI_TRANSCRIPTION=off|locale|deterministe, défaut off) ; endpoint
  /internal/transcrire — l'audio ne survit JAMAIS à l'appel (fichier
  temporaire supprimé quoi qu'il arrive) ; indexer_bilans inclut
  désormais InterventionReport.note anonymisée (champ existant depuis
  R2, jamais eu d'écran jusqu'ici) ; 29 pytest.
- Contrat (77 opérations) : POST /assistant/transcribe (multipart).
- API : proxy multipart vers siop2-ai (WORK_ORDERS.edit — même droit
  que la saisie du bilan) ; 2 tests e2e (80 tests API au total).
- Mobile : expo-audio + expo-file-system, bouton dicter/terminer sur
  l'écran de clôture, purge locale après transcription, « Joindre la
  description à l'OT » (corrige un bug latent : enfilerBilan ignorait
  silencieusement les mises à jour de note).
- Docker : siop2-ai embarque le modèle Whisper au build (1,54→2,19 Go),
  construit et vérifié (transcription réelle en conteneur, non-root).
- Vérifié réellement : transcription fidèle (voix de synthèse
  française) en direct, bout en bout via l'API, dans le conteneur
  Docker construit, et chaîne corpus complète (note → clôture →
  réindexation → recherche sémantique).
- Base de dev locale réinitialisée avec accord explicite du référent
  (prisma migrate reset, bloqué par défaut pour un agent IA) après
  pollution par les tests manuels de la recette terrain précédente.

Reste : test tactile sur iPhone physique (bouton dicter) — bloqué par
une connexion USB qui ne s'est pas rétablie malgré câble/port/
redémarrage essayés à plusieurs reprises, reporté comme la recette
Android.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 11:55:47 +01:00

20 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.
  • 🔄 Reprise ici : test tactile de la dictée sur iPhone (bloqué par une connexion USB qui ne s'est pas rétablie malgré câble/port/redémarrage — reporté comme la recette Android). 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.