É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>
apps/ai : /internal/ask — seuil de pertinence, extraits sourcés ou
refus honnête portant la taille du corpus cherché (D2), rédaction via
le Generateur opt-in ; /internal/suggest — similarité sémantique entre
la description libre et les libellés ACTIFS des référentiels, un code
par champ, confiance FORTE/MOYENNE, « N bilans similaires sur ce
parc ». Sans LLM : déterministe, explicable. 23 pytest.
Contrat (74 opérations) : POST /assistant/ask → AssistantAnswer
(EXTRACTIVE/GENERATED/REFUSAL, extraits cités, corpus cherché) et
POST /assistant/suggest-bilan (codes existants seulement) ; clients
web/mobile régénérés.
API NestJS : module assistant — proxy vers siop2-ai (AI_SERVICE_URL/
AI_SERVICE_TOKEN, ADR-004 §4), permissions matrice (ask=view,
suggest=edit), traduction interne→contrat, 503 propre si service
éteint. 6 e2e sur stub HTTP (76 tests API).
Bug débusqué par la vraie chaîne : fastembed ne norme pas ses
vecteurs — la similarité des suggestions dépassait 1 (pgvector
normalisait dans son opérateur, masquant l'écart). Normalisation à
l'encodage + réindexation : bilans en tête (0.41), refus hors corpus,
scores cosinus ≤ 1.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Serveur : updatedAt exposé au détail OT ; baseUpdatedAt optionnel sur
transition/coche/bilan → 409 « Conflit de version » contextualisé (qui,
quand) ; toute écriture secondaire (coche, bilan, commentaire, conso,
MO) fait avancer la version — sans version fournie, le web est
inchangé. ADR-003 : sécurité & protocole de routage mobile (qui vit où
sur l'appareil, purge complète à la déconnexion — correctif réel : la
file et le cache persisté survivaient au logout).
Mobile : file persistée AsyncStorage rejouée dans l'ordre — succès →
propagation de la version fraîche aux saisies restantes du même OT
(nos écritures ne se conflictent pas entre elles, un écart étranger
reste détecté) ; coupure → tout attend ; refus → CONFLIT, la file
s'arrête, l'humain tranche (voir l'OT / rejouer sur version à jour /
abandonner). Transitions, coches, bilan et photos (D5, compressées
~1600 px) passent par la file avec patch optimiste du cache ; écran
Synchro (badge tabbar ambre/rouge) ; préchargement parc + référentiels
(le bilan hors-ligne a ses vocabulaires).
Recette « mode avion » 13/13 en Expo web piloté : gestes hors-ligne →
3 en file → modification concurrente de Salma → conflit tranché →
serveur Terminé avec bilan. 17 tests jest-expo, 74 tests API.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
apps/mobile (Expo SDK 57, TS strict, workspace pnpm) : connexion
e-mail/mdp + sélecteur démo ADR-002 (uniquement si l'API l'expose),
tabbar de la maquette validée (onglets futurs marqués R4.2/R4.3),
« Ma journée » triée priorité puis échéance (fonction pure testée),
urgence en tête, pastille de synchro.
D1 en actes (lecture) : cache TanStack persisté dans AsyncStorage
(7 jours), NetInfo → onlineManager + bandeau hors-ligne horodaté ;
jeton en SecureStore ; tokens light/dark répliqués et testés ; client
typé régénéré depuis docs/openapi.json (règle d'or).
API : CORS_ORIGINS opt-in (vide par défaut) — Expo web/debug seulement,
le web de prod reste derrière le proxy, les apps natives n'ont pas
d'Origin.
Vérifié 10/10 en Expo web piloté (connexion démo Ahmed → Ma journée
scopée → hors-ligne servie du cache → retour) ; 6 tests jest-expo ;
lint racine étendu (react-hooks) ; job CI mobile, deploy en dépend.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Arbitrage du référent sur la revue pixel : tout corriger, activer la
recherche.
- Stock : filtre fournisseur, sous-seuil en tête, « Entrée de stock »
depuis la liste ; fiche pièce : fournisseur → lien Tiers.
- Statistiques : période 3/6/12 mois (paramètre months au contrat).
- Tiers : rattachements syndic→site (migration r3_recette_fixes,
Location.partnerId gardé CLIENT), éditable sur la fiche site, seedé.
- Bibliothèque : filtre « Rattaché à » + glisser-déposer (modale
préremplie, rattachement toujours requis).
- Recherche globale : GET /search (73 opérations) — familles OT/
ascenseurs/sites filtrées par la matrice, « voir autre » respecté ;
topbar ⌘K, debounce, résultats groupés, navigation clavier.
74 tests API (8 nouveaux sur le scoping de la recherche), 14/14
Playwright dont un parcours « recette corrigée », 18/18 contrôles en
navigateur réel, zéro erreur console.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- FileStorage : putObject/getObjectStream/removeObject, bucket créé au
démarrage — MinIO confiné à son implémentation (règle ESLint intacte)
- documents : 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é),
suppression ; e2e : octets téléchargés identiques aux octets envoyés
- analytics : GET /analytics/summary dérivé du réel — coûts/mois
(mouvements + main-d'œuvre figés), pannes par organe (bilans codés),
taux de préventif, durée moyenne de résolution, top équipements
- générateur OpenAPI : query params, multipart, réponse binaire (71 ops)
- CI : service MinIO (bitnami) sur les jobs api et e2e
- test de régression du tri « Interventions récentes » rendu déterministe
(positions absolues instables sous 12 suites parallèles) ; 58 tests,
6 runs complets consécutifs verts
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Anomalie de recette (référent) : OT assigné à Ahmed, traité, clôturé —
visible dans son tableau de bord mais pas dans celui de Salma ni de
l'admin. Cause : liste triée par statut (Terminés en queue) alors que le
tableau de bord prend les 5 premières lignes ; les listes scopées
(technicien) sont courtes, celles des rôles « voir autre » non.
- tri par dernière activité (updatedAt desc) : un OT fraîchement clôturé
remonte en tête pour tous
- urgences « personne bloquée » ACTIVES épinglées au sommet (une urgence
annulée ne squatte plus la tête — corrigé au passage)
- test de régression dans la recette e2e (position ≤ urgences actives)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- migration : Asset.underContract + WorkOrder.periodKey avec unicité
[assetId, periodKey] — l'idempotence de la génération est EN BASE
(deux appels concurrents ne doublent jamais une grille)
- génération : une grille par appareil sous contrat, périodicités ancrées
sur la mise en service (mensuelles toujours dues), premier contrôle
(toutes les tâches) pour un appareil sans historique, échéance fin de
mois, événement GENERATED ; déclenchement manuel (cron au durcissement)
- gabarits administrables (désactivé = exclu des générations suivantes) ;
GET /preventive/status (générées, terminées, en retard, premiers contrôles)
- compteurs : GET meters (2 compteurs, récents d'abord) ; relevé
strictement croissant, refus motivé sinon
- contrat +7 opérations (48) ; 50 tests verts (94,7 % / 78 %) ; smoke test
prod : juillet généré (9 grilles, 8 premiers contrôles), 2e appel à zéro
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- migration r2_exploitation (10 tables) : WorkOrder (référence séquentielle,
horodatages), WorkOrderEvent, Request (1-1, motif de rejet), ReferenceValue,
InterventionReport (6 FK), TaskTemplate/ChecklistItem, Meter/MeterReading
- contrat : 15 opérations (41 total) ; la table des transitions et les champs
requis du bilan vivent dans @siop/shared ; la fiche OT expose
allowedTransitions + closureBlockers (messages métier)
- API : machine à états stricte ; garde de clôture (bilan 3 champs requis +
checklist sans tâche en attente) ; approbation → OT lié 1-1 (409 si déjà
traitée) ; rejet à motif obligatoire ; scoping « voir autre » sur listes et
accès directs (404 sans fuite) ; validation des valeurs de bilan par champ ;
« personne bloquée » triée en tête côté API
- seed : 31 valeurs de référentiels, 8 gabarits (parachute réglementaire),
OT/demandes/compteurs de la maquette — idempotent
- 45 tests verts (95 % stmts / 79 % branches) dont la recette officielle
rejouée de bout en bout ; smoke test sur build de prod
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- ci-contract : régénère docs/openapi.json + schema.d.ts et échoue au
moindre diff — la règle d'or (ADR-001) devient bloquante
- api : services PostgreSQL 18 + Redis, prisma migrate deploy, typecheck,
Jest avec coverageThreshold global 70 % (mesuré : 97,5 % stmts / 90,7 % branches)
- web : typecheck + vitest + build de production
- test unitaire PermissionsGuard (23 tests au total) ; badge CI au README
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>