# 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-16 — Pr. Daaif (+ Claude) — R3.1 : socle backend de la gestion **Actions** - **Maquettes R3 et les 4 décisions VALIDÉES par le référent** → lancement du socle. - **Modèle** (migration `r3_gestion`, 7 tables) : Partner, Part (SANS colonne de quantité), StockMovement (signé, tracé, PU figé), PurchaseOrder/Line, LaborTime (taux figé), Document (préparé pour R3.2) ; `User.hourlyRate` (taux courant, administrable dans Personnes). - **API** (66 opérations au contrat) : tiers, pièces (stock = Σ mouvements calculé en `groupBy`, alerte sous seuil), entrée/ajustement (motif requis, **stock jamais négatif** — vérifié en transaction), BC (Brouillon→Envoyé→Reçu ; **la réception crée les RECEIPT et met à jour `lastUnitPrice`**), consommation sur OT (stock suffisant + **prix figé**) et main-d'œuvre (**taux figé**, refus motivé si taux non défini) ; `WorkOrderDetail.costs` (lignes + total immuables) ; coûts verrouillés après clôture/annulation (409). - **Seed** : 5 tiers, 5 pièces de la maquette (P-0113 sous seuil via son histoire de mouvements), BC reçu + BC envoyé, taux horaires, et **la carte maquette rejouée : OT-0341 = 505 MAD** (240 + 85 + 180) — vérifiée par test. - **55 tests verts** (92 % / 74,9 %) dont la recette officielle : consommer sous seuil → alerte → BC → réception → réappro, prix d'hier intact sur l'OT pendant que le prix courant change ; hausse du taux d'Ahmed sans effet sur les OT passés. **Leçon majeure (durcissement)** - Les références « max+1 » (OT/DEM/BC/P) étaient une **course sous charge parallèle** (500 sporadiques malgré les retries). Remplacées par des **séquences Postgres** (`nextval`) initialisées au max existant : la classe de bugs disparaît — 3 runs Jest complets consécutifs verts. La numérotation ne se remet pas à zéro chaque année (l'unicité prime), consigné. **Prochaine étape** : R3.2 — bibliothèque de documents (upload multipart 20 Mo → `FileStorage.putObject`, téléchargement streamé via l'API — MinIO jamais exposé) + analytics (`GET /analytics/summary`), puis R3.3 écrans web. --- ## 2026-07-16 — Pr. Daaif (+ Claude) — R3 ouverte : maquettes de la gestion à valider **Actions** - **R3 « Gestion » ouverte — design d'abord** : `maquette-r3.html`, **7 écrans** dans le moule validé : Stock (dérivé des mouvements, alertes sous seuil avec « Préparer le BC »), Fiche pièce (les mouvements SONT le stock), Bons de commande (Brouillon → Envoyé → Reçu ; la réception crée les entrées), Coûts sur OT (la carte validée R0 devient réelle : consommation à **prix figé**, main-d'œuvre à **taux figé**), Statistiques (coûts, pannes par organe alimenté par les bilans codés, taux de préventif, top équipements — palette CVD), Tiers (annuaire minimal fournisseurs/syndics), Bibliothèque (documents typés rattachés appareil/OT, MinIO via FileStorage — futur corpus RAG R5). - Rendu vérifié (7 écrans + thème sombre, zéro erreur). **Décisions (proposées à la validation)** - **Stock strictement dérivé des mouvements** (décision v1 éprouvée) : aucune saisie directe de quantité ; les ajustements d'inventaire sont des mouvements motivés. - **Prix figé à la consommation** (dernier prix d'achat) et **taux horaire figé** par personne (champ géré dans Personnes) : le coût d'un OT ne bouge plus jamais. - La réception d'un BC crée les mouvements d'entrée et fige le PU. - Documents : types fermés (Notice / Certificat / Photo / Autre), 20 Mo max, rattachement obligatoire (appareil ou OT). **⛔ Bloquant** : validation des maquettes R3 par le référent avant toute ligne de code R3. --- ## 2026-07-16 — Pr. Daaif (+ Claude) — 🏁 R2 CLOSE : recettée, déployée, taguée **Actions** - **Recette R2 prononcée par le référent** (une anomalie détectée et corrigée en cours de recette — voir entrée précédente : tri des « Interventions récentes »). - **Déploiement vérifié en ligne** : portail public `/q/A1` opérationnel (résolution QR 200), 13 OT avec l'urgence en tête, préventif de juillet généré sur l'instance (8 grilles, 7 premiers contrôles). - DoD complète → **tag `release/r2`**. Le carnet papier est remplacé : signalement QR sans compte → OT → bilan codé → clôture gardée → suivi, plus préventif idempotent et compteurs. **Prochaine étape** : R3 Gestion — design d'abord : maquettes stock/BC/tiers/coûts OT/analytics/bibliothèque à produire et faire valider. --- ## 2026-07-16 — Pr. Daaif (+ Claude) — Recette R2 : anomalie du référent corrigée **Anomalie (recette du référent)** : un OT créé, assigné à Ahmed, traité et clôturé apparaissait dans le tableau de bord d'Ahmed mais PAS dans celui de Salma ni de l'administrateur. **Diagnostic** : la liste API triait **par statut d'abord** (les Terminés relégués en queue) et le tableau de bord affiche les 5 premières lignes. Ahmed, sans « voir autre », a une liste courte → son OT clôturé restait dans le top 5 ; Salma et l'admin voient tout le parc → l'OT clôturé était éjecté des « Interventions récentes ». (La liste OT filtrait aussi « actifs » par défaut, conforme à la maquette — c'est le tableau de bord qui trompait.) **Correctif** : la liste trie par **dernière activité** (`updatedAt` desc) — un OT qui vient d'être clôturé remonte en tête pour tout le monde ; les urgences « personne bloquée » **actives** restent épinglées au sommet (un OT d'urgence annulé ne squattait plus la tête, corrigé au passage). Reproduit avant / vérifié après sur le scénario exact du référent ; **test de régression** ajouté à la recette e2e (position de l'OT clôturé dans la liste de Salma ≤ nombre d'urgences actives). 50 tests API + 11 Playwright verts. --- ## 2026-07-16 — Pr. Daaif (+ Claude) — R2.4 : portail public QR — l'étiquette prend vie **Actions** - **Trois routes publiques `/portal`** (les seules routes @Public métier de l'API), **throttlées** (`@nestjs/throttler` : 10 signalements/min, 60 lectures/min — première surface sans compte) : résolution du QR (`référence → appareil`), signalement (retourne un **jeton de suivi opaque**), suivi par `référence + jeton` (pas de jeton, pas de lecture — aucune énumération possible). - **Suivi sans compte** : `Request.publicToken` (migration `r2_portail`) ; le téléphone du gardien garde `[référence, jeton]` en localStorage (10 max) ; étapes sans jargon **Reçu → Intervention → Résolu** dérivées de l'OT lié ; un rejet s'affiche « Sans suite : {motif} ». - **Page `/q/{réf}`** fidèle à l'écran 6 validé R0 (mobile d'abord) : équipement prérempli « détecté par le QR », gros interrupteur personne bloquée, description, photo annoncée (upload → R3), messages d'erreur en français métier (QR inconnu, throttling). - **e2e Playwright** : LA boucle produit — le gardien signale sans compte sur `/q/A1`, l'équipe traite (approbation → démarrage → bilan → clôture via API), le gardien recharge et voit ✓ Résolu. 11 tests Playwright verts, 50 tests API, 52 opérations au contrat. Générateur OpenAPI : les paramètres non-`id` ne sont plus typés uuid. **Décisions** - Le portail n'expose JAMAIS de liste : lecture uniquement par jeton individuel. - Throttling au niveau du contrôleur portail seulement (le reste de l'API est derrière JWT). **R2 est fonctionnellement complète** (OT, bilan codé, demandes, portail QR, préventif, compteurs). Prochaine étape : recette R2 avec le référent (revue pixel ↔ maquettes R0+R2), déploiement, tag `release/r2`. --- ## 2026-07-16 — Pr. Daaif (+ Claude) — R2.3 : écrans web de l'exploitation **Actions** - **7 écrans/évolutions fidèles aux maquettes validées** : Liste OT (filtres, strie rouge, « immédiat »), **Fiche OT** (boutons de transition issus d'`allowedTransitions`, clôture grisée avec l'explication de la garde, bilan codé 6 selects alimentés par les référentiels, checklist Fait→N-A→à faire au clic, activité chronologique + commentaires, assignation, annulation motivée), Nouvel OT (interrupteur « personne bloquée » qui force la priorité), Demandes (table + panneau d'approbation, rejet en modale à motif), Préventif (4 tuiles réelles, bouton générer avec résumé « regénérer ne double rien », gabarits administrables), Compteurs (saisie + historique), **Tableau de bord réel** (bandeau urgence cliquable, KPIs, OT par statut, interventions récentes ; coûts et pannes par organe annoncés R3). - **L'urgence traverse l'app** : chip pulsante dans la topbar (rafraîchie 60 s), badges de nav (urgences OT, demandes à traiter), bandeau dashboard, tête de liste. Fiche appareil : historique réel + accès compteurs. Accueil dédié aux rôles sans exploitation (Demandeur). - **Trou de conception débusqué par l'e2e** : le Demandeur n'a pas `ASSETS.view` → son sélecteur d'équipement était vide. Correctif : `GET /assets/options` (authentification seule — les références sont affichées en cabine). 49 opérations au contrat. - **Deux fragilités réelles corrigées dans la génération du préventif** : collision de référence (course avec une création d'OT) désormais RETENTÉE au lieu de sauter silencieusement un appareil ; appareil supprimé entre lecture et écriture toléré (P2003). 3 runs Jest complets consécutifs verts. - **9 tests Playwright verts** dont la recette R2 officielle : Karim signale → Salma approuve+assigne → OT → démarrage → clôture bloquée visible → bilan → clôture → la demande de Karim passe « Résolue » ; + préventif idempotent via l'UI, urgence bout-en-bout, compteur croissant. 50 tests API (94,6 % / 78,9 %). **Prochaine étape** : R2.4 — portail public QR (`/q/{ref}` → signalement prérempli sans compte, suivi sans jargon — écran validé R0), puis recette R2 + déploiement + tag. --- ## 2026-07-16 — Pr. Daaif (+ Claude) — R2.2 : préventif (génération idempotente) + compteurs **Actions** - **Modèle** (migration `r2_preventif_compteurs`) : `Asset.underContract` (contrat préventif, maquette fiche appareil) et `WorkOrder.periodKey` avec **unicité `[assetId, periodKey]` : l'idempotence de la génération est EN BASE**, pas seulement dans le code — deux générations concurrentes ne peuvent pas doubler une grille. - **Génération mensuelle** (`POST /preventive/generate`, 200 idempotent) : une grille par appareil sous contrat ; tâches dues par périodicité **ancrée sur la mise en service** (mensuelles toujours dues) ; appareil sans historique préventif → OT « **Premier contrôle** » avec toutes les tâches ; échéance = fin de mois ; événement GENERATED tracé. Déclenchement manuel en R2 (bouton maquette) — l'automatisation cron/BullMQ viendra avec le durcissement production. - Gabarits administrables (ajout/renommage/désactivation — un gabarit désactivé sort des générations suivantes) ; `GET /preventive/status` (générées, terminées, en retard, premiers contrôles du mois). - **Compteurs** : `GET /assets/{id}/meters` (les 2 compteurs, relevés récents d'abord) ; `POST /assets/{id}/meter-readings` — **relevé strictement croissant**, refus motivé sinon. - Contrat : +7 opérations (48). **50 tests verts** (couverture 94,7 % / 78 %) : idempotence (8 grilles seed, jamais 16), premier contrôle vs grille normale, mois anniversaire (mars 2031 pour A1 : les 3/6/12 mois tombent ensemble), gabarit désactivé exclu, compteur qui refuse de redescendre. Smoke test prod : juillet 2026 généré (9 grilles, 8 premiers contrôles), 2ᵉ appel à zéro. **Leçon** - Les specs Jest partagent la base en parallèle : les assertions e2e doivent porter sur **leurs propres données** (ici les 8 appareils du seed), jamais sur des comptages globaux. **Prochaine étape** : R2.3 — écrans web de l'exploitation (liste/fiche OT, demandes+approbation, nouvel OT, préventif, checklist, compteurs, chip urgence + bandeau tableau de bord). --- ## 2026-07-16 — Pr. Daaif (+ Claude) — R2.1 : socle backend de l'exploitation **Actions** - **Modèle R2** (doc + migration `r2_exploitation`, 10 tables) : WorkOrder (référence séquentielle, horodatages par état), WorkOrderEvent (activité), Request (1-1 vers OT, motif de rejet), ReferenceValue (référentiels du bilan par champ), InterventionReport (6 FK nommées), TaskTemplate/ChecklistItem, Meter/MeterReading. - **Contrat** : 15 opérations (41 total) — la **table des transitions** et les champs requis du bilan vivent dans `@siop/shared` (une seule loi pour l'API et l'UI) ; la fiche OT expose `allowedTransitions` et `closureBlockers` (messages métier). - **API** : machine à états stricte (transition → DONE refusée si bilan incomplet OU checklist avec tâche sans réponse — messages « quoi faire », charte §7) ; approbation de demande = création d'OT lié en 1-1 (double traitement → 409) ; rejet à motif obligatoire ; **scoping « voir autre »** appliqué aux listes ET aux accès directs (404, pas de fuite) ; valeurs de bilan validées champ par champ. - **Seed** : 31 valeurs de référentiels (6 champs), 8 gabarits de préventif (parachute réglementaire), 4 OT et 4 demandes rejouant la maquette, compteurs d'A1. - **45 tests verts** (couverture 95 % / 79 % branches) dont la **recette officielle rejouée** : demande Karim → approbation Salma → OT assigné Ahmed → démarrage → clôture refusée sans bilan → bilan codé → clôture → Karim voit sa demande résolue. Smoke test build prod. **Décisions** - `POST` de transition/rejet répondent **200** (le contrat prime sur le défaut 201 de Nest — c'est le contrat qui a raison). - La priorité « personne bloquée » trie en tête côté API : aucun client ne peut l'oublier. **Prochaine étape** : R2.2 — génération mensuelle du préventif (BullMQ, idempotente, premier contrôle) + API compteurs, puis R2.3 écrans web. --- ## 2026-07-16 — Pr. Daaif (+ Claude) — R2 ouverte : maquettes des écrans manquants à valider **Actions** - **R2 « Exploitation » ouverte — design d'abord.** Les écrans cœur sont déjà validés depuis R0 (tableau de bord, liste OT, fiche OT avec bilan codé 6 champs et garde de clôture, portail demandeur) ; `maquette-r2.html` complète avec les **5 écrans manquants** : Demandes (file + panneau d'approbation : approuver → OT 1-1, rejeter avec motif requis), Nouvel OT (interrupteur « personne bloquée » explicite), Préventif (gabarits à périodicité — parachute marqué réglementaire — génération du mois **idempotente**, premiers contrôles), OT préventif (checklist Fait/N-A, clôture bloquée si tâche sans réponse), Compteurs (relevés croissants, historique). - Rendu vérifié (5 écrans + thème sombre, zéro erreur). **Décisions (proposées à la validation)** - Statuts de DEMANDE distincts (Reçue / Approuvée→OT lié / Rejetée / Résolue) réutilisant la sémantique chromatique des statuts OT ; à trancher : est-ce une entorse à « réservés » de la charte ou une extension cohérente ? - Un rejet de demande **exige un motif** (lisible côté portail, sans jargon). - Génération mensuelle : automatique le 1ᵉʳ à 06 h + bouton manuel ; **regénérer ne crée que le manquant** ; appareil sans historique → OT « premier contrôle » (toutes les tâches). - Relevé de compteur strictement croissant (refus sinon). **⛔ Bloquant** : validation des maquettes R2 par le référent avant toute ligne de code R2. **→ Levé le 16/07/2026 : maquettes R2 et les 4 décisions VALIDÉES par le référent** (statuts de demande = extension cohérente, actée). Lancement R2.1 (socle backend exploitation). --- ## 2026-07-16 — Pr. Daaif (+ Claude) — 🏁 R1 CLOSE : recettée, déployée, taguée **Actions** - **Recette R1 prononcée par le référent** (écrans ↔ maquette-r1, deux thèmes). - **Déploiement vérifié en ligne** () : migration `r1_referentiel` + seed appliqués au boot du conteneur — 5 sites, 8 appareils, écrans /sites servis ; parcours API admin vérifié (locations, assets). - DoD complète (recette, CI 6 jobs verts, revue pixel, production, journal) → **tag `release/r1`**. **Prochaine étape** : R2 Exploitation. Maquettes R0 déjà validées pour tableau de bord, liste OT, fiche OT (bilan codé), portail demandeur ; il restera à maquetter le préventif (gabarits, grille du mois, checklist) avant tout code R2 — design d'abord. --- ## 2026-07-16 — Pr. Daaif (+ Claude) — R1.2 : écrans web du référentiel **Actions** - **7 écrans fidèles à maquette-r1.html** : Sites (liste + **carte Leaflet/OSM réelle**, pins maquette, création en modale avec position posée au clic), Fiche site (arbre site→zones, appareils, ajout de zone), Ascenseurs (table dense, filtres site/statut, strie rouge sur appareil à l'arrêt), Fiche appareil (écran validé R0 : identité, organes ± ajout/retrait, changement de statut, **QR réel** ; historique R2 et documents R3 annoncés sans simulation), Nouvel ascenseur (identité → rattachement → organes ligne à ligne), **Étiquette A6 imprimable** (`window.print` n'imprime qu'elle, objet papier même en sombre), Personnes & équipes (invitation → **lien d'activation affiché à copier** — pas d'email en R1, décision explicite ; renvoi de lien ; équipes), Catégories (ajout, renommage inline, désactivation). - **Page /activation** (cible du lien d'invitation, moule visuel de la connexion) — la personne choisit son mot de passe et entre directement. - **Navigation pilotée par la matrice** : les entrées livrées n'apparaissent que si `canView` (Catégories exige SETTINGS) ; boutons de création/édition conditionnés par `can()` — l'API re-vérifie de toute façon. - **e2e Playwright** : la **recette R1 officielle rejouée intégralement** (site → zone → appareil+organe → étiquette → invitation → activation du compte → l'invité est connecté) + un parcours « la matrice pilote l'UI » (Technicien : lecture sans boutons) ; purge idempotente des données de test en globalSetup. 5 tests verts, 4 vitest, build prod, lint. **Leçon** - Paquet workspace CJS + Vite : les exports nommés runtime échouent (pas de pré-bundle des paquets liés) → alias Vite `@siop/shared` → **source TypeScript** ; l'API garde le dist CJS. Symptôme vicieux : app blanche, tests e2e tous rouges. **Prochaine étape** : R1.3 — recette R1 avec le référent (revue pixel écrans ↔ maquette-r1), déploiement (CI → Dokploy), tag `release/r1`. Puis R2 Exploitation (maquettes déjà validées pour OT/demandes/portail). --- ## 2026-07-16 — Pr. Daaif (+ Claude) — R1.1 : socle backend du référentiel **Actions** - **Modèle R1** (doc + Prisma + migration `r1_referentiel`) : Category (kind EQUIPMENT/COMPONENT_TYPE), Location (site → zone, lat/lng + **colonne PostGIS générée** `geography(Point,4326)` + index GIST), Asset (statut d'équipement), AssetComponent (organe **sans emplacement par construction**), Team (m2m User), invitation sur User (token unique + expiration). Migration **autosuffisante** (`CREATE EXTENSION IF NOT EXISTS postgis`). - **Contrat** : 21 nouvelles opérations (26 au total) — categories/locations/assets+organes/teams/users/roles/invitations/activate ; générateur OpenAPI étendu aux paramètres de chemin ; spec + client web régénérés dans le même commit. - **API** : 4 nouveaux modules + gestion des personnes, tous sous `@RequirePermission` (la matrice décide) ; invariants en service : profondeur 2, kinds de catégories, catégorie jamais supprimée, lien d'activation 7 jours à usage unique qui connecte directement. - **Seed** : parc de la maquette (5 sites + 8 zones, 8 appareils dont B2 à l'arrêt et M1 en maintenance, organes d'A1/B2, 9 catégories, 2 équipes). - **36 tests verts** (couverture 96 % stmts / 85 % branches) : parcours de recette site→zone→appareil→organes, profondeur 3 refusée, matrice vivante (Technicien lit mais ne crée pas), invitation→activation complète (lien périmé/consommé/renvoyé). Smoke test sur build de prod : sites avec compteurs, fiche A1 et ses 4 organes. - **CI** : bascule sur `postgis/postgis:18-3.6` (la migration R1 l'exige) — le moment anticipé dans le commentaire du workflow. **Leçons** - Migration modifiée après application locale ⇒ réaligner son checksum dans `_prisma_migrations` (ou reset) — d'où la règle : rendre la migration autosuffisante AVANT de l'appliquer. - Un serveur `reuseExistingServer` de Playwright peut squatter :3000 et faire tester un dist périmé — tuer le port avant tout smoke test. **Prochaine étape** : R1.2 `apps/web` — écrans Sites (+ carte Leaflet/OSM), Fiche site, Ascenseurs, Nouvel ascenseur, Étiquette QR, Personnes & équipes, Catégories, fidèles à maquette-r1.html. --- ## 2026-07-16 — Pr. Daaif (+ Claude) — R0 CLOSE (tag) · R1 ouverte : maquettes à valider **Actions** - **R0 close** : recette prononcée par le référent, tag `release/r0` poussé (DoD complète : CI verte, revue pixel, production en ligne, journal). - **R1 « Référentiel » ouverte — design d'abord** (principe n°1) : `maquette-r1.html` produite dans le moule validé (CSS répliquée de `maquette-web.html`, mêmes tokens/typo/motifs) — **7 écrans** : Sites (liste + carte PostGIS), Fiche site (hiérarchie d'emplacements), Ascenseurs (liste, statuts d'équipement distincts des statuts OT), Nouvel ascenseur (identité → rattachement → organes), Étiquette QR imprimable (A6 papier, blanche même en thème sombre), Personnes & équipes (invitation par lien d'activation 7 jours, modale montrée), Catégories (référentiels administrables, jamais de suppression si utilisé). - Rendu vérifié en Chrome headless : 7 écrans + 2 contrôles thème sombre, zéro erreur. **Décisions (proposées à la validation)** - Statuts d'équipement (En service / À l'arrêt / En maintenance) **distincts** des statuts OT ; l'appareil à l'arrêt porte la strie rouge. - Un organe **n'a jamais d'emplacement propre** : il suit son appareil (contrainte en base, comme en v1). - Invitation par **lien d'activation** (7 jours) — aucun mot de passe créé pour autrui ; compte inactif avant activation. - Catégorie utilisée : renommage/désactivation seulement, jamais de suppression. **⛔ Bloquant** : validation des maquettes R1 par le référent avant toute ligne de code applicatif R1. **→ Levé le 16/07/2026 : maquettes R1 et les 4 décisions de conception VALIDÉES par le référent.** Lancement R1.1 (socle backend). --- ## 2026-07-16 — Pr. Daaif (+ Claude) — 🚀 R0 EN PRODUCTION : **Actions** - Blocage de clone résolu (le provider « Custom » HTTPS n'avait pas d'identifiants sur dépôt privé) ; déploiement Dokploy réussi depuis GitHub, domaine posé sur `siop2-web:80`, certificat Let's Encrypt émis. - **Vérification de l'instance en ligne** (commit `3f9d0d8`) : `/api/health` → `ok` (base, Redis, MinIO `up`) ; 7 comptes démo ; demo-login → `/users/me` (Salma Idrissi, Dispatcher, 10 lignes de matrice) ; `401` sans jeton ; fallback SPA sur `/design` ; HTTPS valide. - Principe « déployer tôt » honoré : R0 est en ligne avant l'ouverture de R1. **Reste à faire** - Poser le secret `DOKPLOY_WEBHOOK_URL` (runbook §4) pour activer le déploiement continu — le job `deploy` saute proprement tant qu'il manque. - Sauvegardes PostgreSQL côté Dokploy (runbook §5) à configurer. - **Recette R0 avec le référent** sur l'instance en ligne (revue pixel écrans ↔ maquettes) → clôture du jalon R0 → ouverture R1 Référentiel. ## 2026-07-15 — Pr. Daaif (+ Claude) — R0.13 : Dockerfiles + runbook Dokploy **Actions** - **Image API** (multi-stage, node:24-slim) : `pnpm deploy --legacy --prod`, client Prisma régénéré dans l'arborescence déployée, entrypoint `prisma migrate deploy` → seed optionnel (`SEED_ON_START=true`, compilé en `dist/seed`) → API ; utilisateur non-root, HEALTHCHECK `/health`. `prisma` passe en dépendance de production (migrations au boot). - **Image web** (nginx:alpine) : statique Vite + proxy `/api` → `${API_UPSTREAM}` (template envsubst) — même topologie que le proxy Vite de dev ; cache immuable sur `/assets`, `no-cache` sur `index.html`. - **`infra/docker-compose.dokploy.yml`** : 5 services préfixés `siop2-`, secrets exigés (`:?`), profils production client / instance démo documentés dans le fichier ; seul `siop2-web` rejoint `dokploy-network` (l'API n'est jamais exposée). - **Runbook** `docs/06-production/runbook-dokploy.md` : topologie, variables, checklist de première mise en production, releases suivantes, rollback, répétition locale. - **Répétition locale validée** : images construites, conteneurs lancés sur le réseau infra (migrate + seed au boot), parcours complet vérifié à travers nginx conteneurisé (`/api/health` tout `up`, demo-login → `/users/me`, fallback SPA). **Leçons de la répétition (consignées pour le playbook)** - **Prisma en conteneur** : `binaryTargets` explicites dans `schema.prisma` (`debian-openssl-3.0.x` x64 serveur + `linux-arm64-openssl-3.0.x` répétition Mac) — sinon mismatch de moteur au runtime. - **nginx** : upstream résolu **à la requête** (resolver `127.0.0.11` + variable) et non au boot — sinon nginx refuse de démarrer si l'API n'est pas encore là. - **Healthcheck alpine** : `127.0.0.1` et non `localhost` (busybox wget tente ::1, nginx écoute en IPv4). - **Le double verrou ADR-002 a été prouvé en vraie situation** : l'image (NODE_ENV=production) avec `DEMO_MODE=true` sans `DEMO_MODE_I_KNOW` **refuse de démarrer** — comportement observé, pas seulement testé. **Décisions** - Migrations **au démarrage du conteneur** (idempotentes) : la base suit toujours le code déployé ; un échec de migration arrête l'API sans servir de trafic. - Le web est l'unique service exposé (domaine → nginx → proxy interne `/api`) : mêmes chemins en dev et en prod, surface d'attaque minimale. **Statut** : R0.13 prêt — la première exécution réelle attend les **accès au serveur du partenaire**. Reste pour clore R0 : recette avec le référent (revue pixel écrans ↔ maquettes). --- ## 2026-07-15 — Pr. Daaif (+ Claude) — R0.13 (suite) : instance ENSET + déploiement continu **Actions** - **Instance de démonstration décidée** : `https://siop2.apps.enset.top` (Dokploy ENSET, projet compose créé par le référent) — la production client SPELEV suivra la même procédure avec le profil « production client ». - Job **`deploy`** ajouté au pipeline : appelle le webhook Dokploy sur push `main` **uniquement si lint + contrat + api + web + e2e sont verts** ; « skip » explicite tant que le secret `DOKPLOY_WEBHOOK_URL` n'est pas configuré. L'« Auto Deploy » natif de Dokploy reste désactivé (il ignorerait la CI). - Runbook §3 réécrit en checklist concrète (source GitHub, Compose Path, variables du profil démo, domaine → `siop2-web:80`) et §4 en procédure CD (secret webhook). **Prochaine étape** : premier déploiement manuel (runbook §3), pose du secret `DOKPLOY_WEBHOOK_URL` (§4), vérification `https://siop2.apps.enset.top/api/health`, puis recette R0 avec le référent sur l'instance en ligne. --- ## 2026-07-15 — Pr. Daaif (+ Claude) — R0.12 (2/2) : ESLint + parcours Playwright — R0.12 CLOS **Actions** - **ESLint 10** (flat config unique à la racine, typescript-eslint, react-hooks sur le web) + scripts `lint` par paquet et job CI dédié. La règle d'architecture ADR-001 est codée : `no-restricted-imports` interdit `minio` partout **sauf** `minio-storage.service.ts` — violation vérifiée par un fichier-test (détectée puis retiré). Monorepo lint : 0 erreur. - **Playwright** (`apps/web/e2e`) : 3 tests — redirection sans jeton (fermée par défaut), parcours complet connexion démo → coquille (rail actif, chip DÉMO) → **bascule de rôle chronométrée < 3 s** (ADR-002) → /design, et déconnexion avec purge de session. `webServer` démarre l'API construite + Vite ; seed idempotent en globalSetup ; localement `PW_CHANNEL=chrome` (pas de téléchargement). - CI : jobs `lint` et `e2e` (services PostgreSQL/Redis, artefact playwright-report en cas d'échec). Vitest restreint à `src/` (e2e appartient à Playwright). **Décisions** - Une seule config ESLint à la racine (pas une par app) : les règles d'architecture sont transversales et la config est le lieu du cours. **Prochaine étape** : R0.13 — Dockerfiles (api, web) + runbook Dokploy (`docs/04-*`), puis recette R0 avec le référent (revue pixel écrans ↔ maquettes). --- ## 2026-07-15 — Pr. Daaif (+ Claude) — R0.12 (1/2) : pipeline GitHub Actions **Actions** - `.github/workflows/ci.yml`, 3 jobs sur push main + PR : **ci-contract** (régénère `docs/openapi.json` + `schema.d.ts`, échoue au moindre diff — la règle d'or devient bloquante), **api** (PostgreSQL 18 + Redis en services, `prisma migrate deploy`, typecheck, Jest avec **couverture ≥ 70 % bloquante** via `coverageThreshold`), **web** (typecheck + vitest + build prod). - Test unitaire de `PermissionsGuard` ajouté (aucune route `@RequirePermission` en R0 ne l'exerçait) : couverture 97,5 % stmts / 90,7 % branches, 23 tests. - Badge CI dans le README ; `ci-contract` simulé en local (diff propre) avant push. **Décisions** - CI sur PostgreSQL nu en R0 (la migration `r0_identity` n'exige aucune extension) ; bascule sur l'image `infra/postgres` dès que des tests toucheront pgvector/PostGIS (R1+). - Pas de MinIO en CI : `/health` répond `degraded` sans casser les tests — seul `database: up` est exigé. **Prochaine étape** : R0.12 (2/2) — ESLint (dont la règle « pas d'import MinIO hors FileStorage »), parcours Playwright (connexion démo → coquille → bascule de rôle), puis R0.13 Dockerfiles + runbook Dokploy. --- ## 2026-07-15 — Pr. Daaif (+ Claude) — R0.11 : apps/web (connexion + sélecteur démo, coquille, /design) **Actions** - `apps/web` (React 19 + Vite + Tailwind v4) : `tokens.css` copié tel quel depuis 02-design ; classes composants **extraites de la maquette validée** ; Manrope auto-hébergée (@fontsource) ; primitives shadcn-style possédées (Button/variants cva, menu Radix). - **Client typé** : `pnpm generate:client` → `src/api/schema.d.ts` généré depuis `docs/openapi.json` et committé (règle d'or) ; openapi-fetch + injection du jeton. - Écrans : **connexion** (fidèle maquette, liste démo masquée si l'API répond 404 — ADR-002), **coquille** sidebar (4 groupes, rail safran actif, écrans à venir marqués R1-R3) + topbar (recherche ⌘K, bascule de thème, chip DÉMO, **sélecteur de rôle** dans le menu compte), **/design** (référence vivante : nuanciers, typo, boutons, pastilles), tableau de bord R0 minimal. - Compléments : tokens `--nav-*` ajoutés à `tokens.css` (valeurs issues de la maquette validée) ; **seed réaligné sur les personas de la maquette** (Salma Idrissi, Ahmed Benali, …) pour des revues pixel cohérentes. - **Vérifié dans un vrai navigateur** (Playwright + Chrome headless) : parcours démo-login → tableau de bord → bascule Dispatcher→Technicien en **101 ms** (critère < 3 s) → /design clair & sombre ; 7 captures, zéro erreur console. Typecheck, 3 tests vitest, build prod OK. **Décisions** - Le web ne lit pas `VITE_DEMO_MODE` : la présence du mode démo est déduite de la réponse de `/auth/demo-accounts` (404 ⇒ rien n'est affiché) — une seule source de vérité, l'API. - Les entrées de navigation des releases futures restent visibles mais inertes, marquées R1/R2/R3 — le périmètre est annoncé, pas simulé. **Prochaine étape** : R0.12 — CI GitHub Actions (lint, typecheck, tests, couverture api ≥ 70 %, `ci-contract` sur openapi.json/clients, Playwright du parcours démo) puis R0.13 Dockerfiles + runbook Dokploy. --- ## 2026-07-15 — Pr. Daaif (+ Claude) — R0.10 : apps/api complète (auth, matrice, démo-login, seed) **Actions** - `packages/shared` : vocabulaires (7 rôles, 10 catégories d'objets), schémas Zod (auth, profil, santé) et **contrat d'API** ; `pnpm contract` génère `docs/openapi.json` (committée — règle d'or ADR-001). - `apps/api` (NestJS 11 + Prisma 6) : migration `r0_identity` (Role/Permission/User) ; **guard JWT global fermé par défaut** (+ `@Public()` explicite) ; **PermissionsGuard** (`@RequirePermission`, matrice relue en base, cache 60 s) ; `FileStorage` (seul point d'import MinIO) ; `/health` (base, Redis, stockage). - **Démo-login ADR-002** : module enregistré uniquement si `DEMO_MODE=true` (sinon routes **404**), double verrou production (`DEMO_MODE_I_KNOW`), refus des comptes `isDemo=false`. - **Seed idempotent** : 7 rôles, matrice complète (70 lignes), 7 comptes démo (mot de passe commun `SEED_DEMO_PASSWORD` pour la connexion classique). - **Vérifié bout-en-bout** : 19 tests Jest verts (dont e2e démo on/off) ; smoke test sur build de prod — démo-login → `/users/me` avec matrice, 401 sans jeton, health `ok`. **Décisions** - `AppModule.forRoot()` (module dynamique) pour rendre l'enregistrement conditionnel du module démo **testable dans les deux états** — le e2e « routes absentes » est l'exigence n°1 de l'ADR-002. - Le seed n'écrase jamais une ligne de matrice existante : **la base est la source de vérité des droits**, le fichier n'est que le point de départ. **Prochaine étape** : R0.11 `apps/web` — login + sélecteur de comptes démo (fidèle à maquette-web.html), coquille sidebar/topbar avec bandeau « DÉMO », page /design, client typé généré depuis `docs/openapi.json`. --- ## 2026-07-15 — Pr. Daaif (+ Claude) — R0 : maquettes VALIDÉES ; architecture + squelette **Actions** - **Maquettes HD validées par le référent** → le design est la loi des revues pixel. - 03-architecture : ADR-001 (stack), ADR-002 (démo-login `DEMO_MODE`, double verrou prod), vue C4, modèle de données R0 (Role/Permission/User, `isDemo`). - Racine monorepo (pnpm + turbo, Node 24) ; `infra/` : compose local (PostgreSQL 18 pgvector+PostGIS via Dockerfile dédié, Redis, MinIO). **Décisions** - **Convention `siop2-`** pour tous services/conteneurs Docker (référent — collisions sur le réseau partagé Dokploy en v1). **Prochaine étape (reprise)** : R0.10 `apps/api` (auth JWT + matrice permissions + démo-login + seed) → R0.11 `apps/web` (login + sélecteur démo + coquille + /design) → R0.12 CI → R0.13 prépa Dokploy. --- ## 2026-07-15 — Pr. Daaif (+ Claude) — R0 : kickoff, vision, cadrage, design **Actions** - **Refondation décidée** : nouveau dépôt `siop-spelev/siop2`, 100 % nouveau code ; la v1 (`siop`) est gelée comme référence. Jalons R0→R5 créés. - Playbook initialisé : **00-vision** (charte projet, personas, benchmark Atlas), **01-cadrage** (plan de releases avec DoD, user-story map, exigences non fonctionnelles mesurables, registre des risques). - **02-design** : charte graphique (bleu-treuil / safran / acier, Manrope, motif « rail »), `tokens.css` (source de vérité, bi-thème), palettes analytics **validées par script** (CVD + contraste, modes clair et sombre), **maquettes HD** navigables (7 écrans : connexion + démo-login, tableau de bord, liste OT, fiche OT avec bilan codé, fiche ascenseur + QR, portail demandeur, mobile technicien) — publiées pour revue. **Décisions (référent, via questions)** - Diagnostic v1 : esthétique absente, docs éparpillées, périmètre dérivant, bascule de comptes pénible → V2 **design-first**, playbook en livre, releases fermées, **sélecteur de compte démo** (`DEMO_MODE`) dès R0. - 100 % nouveau code ; périmètre v1 = web + mobile + IA par paliers ; déploiement Dokploy ; nom conservé : **SIOP**. **Prochaine étape** : validation de la charte + maquettes par le référent (**bloquant** — aucun code applicatif avant), puis 03-architecture + squelette monorepo. ---