# Opérations et Maintenance ## Correctifs et dépannage documentés ### Ancrage notarié (prod) : échec déchiffrement ou filigrane PDF (catalog.Pages, Invalid object ref) **Problème :** En prod, les logs remontent des erreurs lors de l’ancrage de documents notariés : - `Unsupported state or unable to authenticate data` (Decipheriv.final) puis `Fichier corrompu ou modifié (vérification d'intégrité échouée)` ; - `[WatermarkService] Erreur lors de l'ajout du filigrane: _this.catalog.Pages is not a function` ; - `Trying to parse invalid object`, `Invalid object ref: 74 0 R`, `Buffer commence par %PDF mais n'est pas un PDF valide`. **Cause :** Deux cas distincts (éventuellement sur deux documents ou deux fichiers différents) : 1. **Échec déchiffrement** : La clé d’office ou la clé globale ne permet pas de déchiffrer le fichier (GCM auth tag invalide). Causes possibles : fichier chiffré avec une autre clé (migration, rotation), payload corrompu ou tronqué, clé d’office incorrecte en BDD. 2. **PDF invalide après déchiffrement** : Le buffer déchiffré commence par `%PDF` mais la structure du PDF est invalide ou non supportée par pdf-lib (référence d’objet cassée, catalog/Pages absent). Causes possibles : PDF corrompu, généré par un outil exotique, ou contenu déchiffré avec une mauvaise clé (garbage qui commence par %PDF). **Root cause :** Pour (1) : mauvaise association clé/fichier ou donnée chiffrée corrompue. Pour (2) : format PDF non standard ou contenu réellement corrompu après un déchiffrement réussi. **Correction :** - Enrichissement des erreurs avec l’UID du fichier (`File : ...`) dans `FilesNotaryService` (déchiffrement et fallback clé globale) pour faciliter le diagnostic en logs. - Message d’erreur explicite lors de l’échec du filigrane si la structure PDF est invalide (catalog.Pages, Invalid object ref) : indication « PDF invalide ou structure non supportée ; possible fichier corrompu ou déchiffré avec mauvaise clé ». - Dans `DocumentAnchoringWatermarkHelper`, encapsulation de l’erreur de téléchargement/déchiffrement avec le `fileUid` dans le message (`Failed to download or decrypt file : ...`). **Modifications :** - `lecoffre-back-main/src/services/common/FilesNotaryService/FilesNotaryService.ts` : en cas d’échec déchiffrement (direct ou après fallback), lever une erreur avec préfixe `File ${uid}:`. - `lecoffre-back-main/src/services/common/WatermarkService/helpers/WatermarkPdfProcessorHelper.ts` : dans le catch de `addWatermark`, si le message contient `catalog`, `Pages is not a function` ou `Invalid object ref`, ajouter un hint dans le message et le log. - `lecoffre-back-main/src/services/common/WatermarkService/helpers/WatermarkBufferProcessorHelper.ts` : dans `validatePdfBuffer`, même hint lorsque `PDFDocument.load` échoue avec une de ces chaînes. - `lecoffre-back-main/src/services/notary/DocumentAnchorsService/helpers/DocumentAnchoringWatermarkHelper.ts` : `.catch` sur `filesService.download(fileUid)` pour renvoyer une erreur incluant `fileUid`. **Impact :** Les logs en prod contiennent l’UID du fichier en erreur et un message plus explicite pour les PDF invalides. Comportement fonctionnel inchangé : l’ancrage échoue toujours pour ces cas ; le diagnostic est amélioré. **Modalités de déploiement :** Déploiement standard backend (build-and-deploy). **Modalités d’analyse :** Consulter les logs backend (notary, ancrage) ; en cas d’erreur d’ancrage, le message doit contenir `File :` ou `Failed to download or decrypt file :` et, pour les PDF invalides, le hint sur structure non supportée / mauvaise clé. Vérifier en BDD que la clé d’office du document (`office_encryption_key`) et la clé du fichier (`files_notary.key`) sont cohérentes pour le fichier concerné. ### Email de préférence pour les envois non renseigné à l’ajout d’un confrère (annuaire) **Problème :** Lors de l’ajout d’un confrère depuis l’annuaire (recherche notaire), le champ « Email de préférence pour les envois » restait vide au lieu d’être pré-rempli avec l’email du notaire sélectionné. **Cause :** La recherche utilise l’API Annuaire V2 : d’abord l’endpoint PB lookup (`/api/v2/directory/lookup`), puis en fallback l’endpoint PP personnes (`/api/pp/v2/personnes`). Les résultats issus du lookup sont construits à partir de `personnes` et de `rattachements`. Pour les entrées construites uniquement à partir de `personnes`, la fonction `buildResultFromPersonne` fixait l’email à une chaîne vide et ne lisait pas d’éventuels champs `email`/`mail` renvoyés par l’API. De plus, lorsque le lookup renvoyait des résultats, l’appel PP (qui fournit les emails via `mailRattachement`/`email`/`mail`) n’était jamais effectué, donc aucun enrichissement n’était possible pour ces cas. **Root cause :** (1) Non-utilisation des champs email/mail sur l’objet personne dans la réponse lookup. (2) Absence d’enrichissement des résultats lookup par un appel PP pour compléter les emails manquants. **Correction :** - Dans `buildResultFromPersonne`, extraction de l’email depuis l’objet personne via `pickFirst(..., "email", "mail", "courriel")` au lieu d’une chaîne vide ; types `AnnuaireLookupPerson` et paramètre de `buildResultFromPersonne` étendus avec `mail?` et `email?`. - Après le PB lookup, si des résultats existent, appel d’une nouvelle méthode `enrichLookupResultsWithPpEmails` : un appel unique à l’endpoint PP (première page) avec la même requête (prenom, nomUsuel), construction d’une map idNot → email, puis mise à jour des résultats du lookup qui ont un idNot mais un email vide. **Modifications :** - `lecoffre-back-main/src/services/common/IdNotDirectoryService/helpers/IdNotDirectoryApiSearchHelper.ts` : types `AnnuaireLookupPerson` et paramètre de `buildResultFromPersonne` avec `mail?`, `email?` ; `buildResultFromPersonne` utilise `pickFirst` pour l’email et l’inclut dans le `displayName` de fallback ; nouvelle méthode privée `enrichLookupResultsWithPpEmails` ; après le lookup réussi, appel à cette méthode avant de retourner. **Impact :** Lorsqu’un confrère est choisi dans l’annuaire, l’email de préférence pour les envois est pré-rempli lorsque l’API (lookup ou PP) fournit un email. Comportement inchangé pour les résultats déjà fournis par les rattachements ou par le fallback PP seul. **Modalités de déploiement :** Déploiement standard backend (build-and-deploy). **Modalités d’analyse :** Depuis l’interface d’ajout de confrères à un dossier, effectuer une recherche par nom de notaire, sélectionner un résultat : le champ « Email de préférence pour les envois » doit être renseigné lorsque l’annuaire retourne un email pour ce notaire. Vérifier en logs backend les messages d’enrichissement (`Enrichissement email lookup par PP`) si des résultats lookup sont complétés par PP. ### Erreur en mode « Dossiers invités » sur la page /folders/shared (prod) **Contexte lint backend (2026-03-12) :** Après corrections fix-lint (variables inutilisées, non-null assertions, extraction `scheduleV1ToV2SyncIfEnabled`), il reste environ 334 erreurs ESLint (max-lines, max-params, complexity). Plan : refactor par lots ; pas de désactivation de règles. Détail dans cette section si besoin. **Problème :** Un notaire invité (ex. gwendal@lecoffre.io) en mode « Dossiers invités » obtient une erreur en accédant à la page des dossiers partagés (réponse à une invitation d'un office, ex. DIADEYS). La page affiche « Impossible de charger les dossiers partagés » ou un écran d'erreur. **Cause :** La page `/folders/shared` (`shared.tsx`) charge la liste via `GET /notary/folders/shared`. Le backend rejette cette requête lorsque l’office actif est `GUEST_FOLDERS` (400 : « Active office is required to list shared folders »). Pour un notaire invité, l’office actif est justement `GUEST_FOLDERS`. **Root cause :** `GET /notary/folders/shared` est conçu pour lister les dossiers partagés **avec un office réel**. En mode invité, la liste des dossiers accessibles doit provenir de `GET /notary/folders` (filtré par email/idNot via le JWT), pas de `GET /notary/folders/shared`. **Correction :** Dans `lecoffre-front-main/src/pages/folders/shared.tsx`, lorsque l’utilisateur est en `GUEST_FOLDERS` (permissionContext ou activeOfficeStore), ne plus appeler `getSharedFolders()`. Afficher `DefaultNotaryDashboard` sans fournir de liste externe : le tableau de bord charge alors la liste via `GET /notary/folders` (même flux que la page principale des dossiers), ce qui fonctionne pour les notaires invités. **Modifications :** - Fichier : `lecoffre-front-main/src/pages/folders/shared.tsx` - Import de `useRolePermissions` et `activeOfficeStore`. - Détection de `GUEST_FOLDERS` via `permissionContext?.activeOfficeUid` ou `activeOfficeStore.activeOffice?.office_uid`. - Si `GUEST_FOLDERS` : rendu de `DefaultNotaryDashboard` sans prop `folders` (chargement interne via GET /notary/folders). - Sinon : comportement inchangé (chargement via `getSharedFolders()`, mapping en `OfficeFolderExtended`). **Impact :** Les notaires invités peuvent accéder à la page « Dossiers partagés » et voir la liste de leurs dossiers invités sans erreur. Aucun changement pour les offices réels. **Modalités de déploiement :** Déploiement standard frontend (build-and-deploy). **Modalités d'analyse :** Se connecter avec un compte notaire invité (ou un compte ayant des dossiers partagés et sélectionner « Dossiers invités »), ouvrir la page /folders/shared : la liste des dossiers doit s’afficher sans erreur. ### Évolution – Personne en charge du dossier (main_contact_uid) – 2026-03-12 **Objectif :** Afficher dans l’espace client/tiers la personne en charge du dossier (email, téléphone) et permettre de la désigner/modifier dans l’écran « Modifier les collaborateurs ». **Modifications :** Nouveau champ `main_contact_uid` sur `office_folders` (nullable, FK `users`). Création dossier : `main_contact_uid` = créateur. PUT dossier : accepte `main_contact_uid` (validé parmi les stakeholders). GET dossier : retourne `main_contact`. Front : ContactBox utilise main_contact puis created_by puis notaire puis premier stakeholder ; écran collaborateurs : liste « Personne en charge », envoi de `main_contact_uid` à l’enregistrement. **Modalités de déploiement :** Appliquer la migration Prisma `20260312120000_add_main_contact_uid_to_office_folders`. Puis déploiement standard backend + frontend. **Modalités d’analyse :** En espace client/tiers, vérifier que le contact affiché correspond à la personne en charge si définie. Sur « Modifier les collaborateurs », choisir une personne en charge, enregistrer, vérifier en espace client que le contact affiché est bien celui choisi. ### Migration V1 → V2 : Phase 4 (réancrage) non bloquante **Problème :** Lors de l'import V1 → V2, la Phase 4 (réancrage des documents et RIB) pouvait faire échouer toute la migration (exit 1) lorsque des documents V1 étaient invalides (fichier non-PDF, corrompu, format tableur, PDF chiffré). La Phase 5 (vérification du statut) n'était alors jamais exécutée et le log restait sans clôture explicite. **Cause :** Le script `import-v1.sh` utilise `set -e`. Dès que `ssh_run` (Phase 4) renvoyait le code de sortie non nul de `reanchor-all.sh`, le script quittait immédiatement sans exécuter `phase4_rc=$?` ni la Phase 5. **Cause additionnelle (backend) :** Le script backend `reanchor-all-complete.ts` utilisait `files.key IS NOT NULL` comme critère V1 (et `IS NULL` comme critère V2). Or des fichiers V2 peuvent avoir un `key` non nul (compatibilité API), ce qui élargissait la suppression/réancrage au-delà du périmètre V1. **Correction :** En cas d'échec de la Phase 4, le script affiche un avertissement et continue vers la Phase 5 au lieu de quitter. Les erreurs sur documents individuels (PDF invalide, format non supporté) restent loggées dans le fichier de log complet (`./logs/import-v1--.log`). Côté backend, le périmètre V1 est désormais détecté via le format de clé V1 (UUID). Le traitement Phase 4 est idempotent pour les documents : aucune ancre existante n'est supprimée et un document déjà présent avec une entrée d'ancre en base (même incomplète) n'est pas retenté. **Modifications :** - Fichier : `deploy/scripts_v2/import-v1.sh` - Désactivation temporaire de `set -e` autour de l'appel Phase 4 : `set +e` avant `ssh_run`, `set -e` après capture de `phase4_rc=$?`, afin que les échecs partiels (documents ignorés/skip) ne fassent pas quitter le script. - Capture du code de sortie de `ssh_run` pour la Phase 4 (`phase4_rc=$?`). - Si `phase4_rc -ne 0` : `warning "Phase 4 a rencontré des erreurs (documents invalides ou réancrage partiel), continuation vers Phase 5..."` sans `exit 1`. - Si `phase4_rc -eq 0` : `success "Phase 4 terminée avec succès"` comme avant. **Impact :** La migration complète (phases 0 à 5) se termine même lorsque des documents ne peuvent pas être réancrés. Le log complet et la Phase 5 permettent d'identifier les fichiers non migrés ou en erreur. **Modalités de déploiement :** Aucune (script d'import uniquement). Déploiement standard si le script est versionné. **Modalités d'analyse :** Relancer une migration test ; en cas d'erreurs Phase 4, vérifier que le message d'avertissement s'affiche et que la Phase 5 est bien exécutée. Consulter `./logs/import-v1--.log` pour le détail des erreurs document par document. Depuis le backend, les erreurs de conversion PDF affichent aussi une `signatureHex` et un type détecté (ex: zip/7z/png/jpeg/gzip) pour distinguer un faux PDF (mimetype incorrect) d'un contenu corrompu/déchiffré avec une mauvaise clé (cas V1). **Détails (backend) :** Backend : analyseDecryptionError, périmètre V1 par format de clé UUID, skip documents invalides. ### Phase 5 (vérification migration) : erreur « backend.env ligne 7 : 7 : Aucun fichier ou dossier » **Problème :** En prod, le déploiement échouait en Phase 5 (vérification du statut de migration) avec code de sortie 1 et le message : `/etc/lecoffreio/prod.lecoffreio.4nkweb.com/backend.env: ligne 7: 7: Aucun fichier ou dossier de ce type`. La Phase 5 n'était pas exécutée. **Cause :** Le script `check-v1-migration-status.sh` est exécuté sur le serveur (via SSH) et chargeait `backend.env` avec `source "$BACKEND_ENV_FILE"`. Toute ligne du fichier qui n'est pas une assignation valide (ex. une ligne contenant uniquement « 7 » ou une valeur mal formée) est alors interprétée par le shell comme une commande ; le shell tente d'exécuter « 7 » comme nom de commande ou de fichier, d'où l'erreur. **Cause racine :** Le fichier `backend.env` sur l'hôte prod contenait à la ligne 7 une entrée invalide (valeur seule, numéro, ou ligne exécutable). Les fichiers `.env` de prod ne sont pas modifiables depuis le dépôt. **Correction :** Dans `deploy/scripts_v2/check-v1-migration-status.sh`, remplacer `source "$BACKEND_ENV_FILE"` par un chargement qui n'exporte que les lignes au format `VAR=value` (regex `^[A-Za-z_][A-Za-z0-9_]*=`), en ignorant les commentaires et les lignes vides ou non assignation. Ainsi, une ligne 7 invalide dans `backend.env` n'est plus exécutée et ne fait plus échouer la Phase 5. Par ailleurs, la Phase 5 utilise uniquement `APP_ROOT/.secrets/${ENV}/.env.${ENV}.connectDB` (source de vérité). Après migration, backend.env ne doit plus exister (sauvegarde puis suppression) ; la seule source est connectDB. **Modifications :** - Fichier : `deploy/scripts_v2/check-v1-migration-status.sh` - Remplacement de `source "$BACKEND_ENV_FILE"` par une boucle `while IFS= read -r line` qui ne traite que les lignes correspondant à une assignation valide et les exporte. - Utilisation exclusive de `APP_ROOT/.secrets/${ENV}/.env.${ENV}.connectDB`. backend.env ne doit plus exister après migration (sauvegarde puis suppression). **Impact :** La Phase 5 s'exécute même si `backend.env` contient des lignes invalides. Aucune modification des fichiers `.env` prod depuis le dépôt. **Modalités de déploiement :** Déploiement standard (scripts_v2). Aucune action requise sur l'hôte prod pour les `.env`. **Modalités d'analyse :** Relancer le déploiement prod ; la Phase 5 doit se terminer sans erreur « ligne 7 ». Optionnel : sur l'hôte prod, corriger manuellement la ligne 7 de `backend.env` pour supprimer la valeur invalide. ### Dossier sans client : aucun membre affiché après ajout d'un tiers (notaire) **Problème :** En tant que notaire, lorsqu'on crée un dossier sans client puis qu'on ajoute un tiers, aucun membre n'apparaît dans le dossier (vue détail avec onglets Clients / Notaires invités / Tiers). **Cause :** Le critère « le dossier a des participants » (`doesFolderHaveParticipants`) ne prenait en compte que : présence d'au moins un client, ou d'un partage (notaire invité), ou de **documents déposés par des tiers**. Un dossier avec uniquement des tiers associés (sans document déposé encore) n'était pas considéré comme ayant des participants, donc la vue détail n'était pas rendue. **Cause racine :** Dans `useFolderData.ts`, `doesFolderHaveParticipants` utilisait `hasThirdPartyDocuments` (documents des tiers) et non la liste des **tiers associés au dossier** (`folder.third_parties`). L'API détail folder inclut déjà `third_parties: true` ; la donnée était disponible mais non utilisée pour ce calcul. **Correction :** Inclure la présence de tiers associés au dossier (`folder.third_parties`) dans le calcul de `doesFolderHaveParticipants`, en plus des clients, des folder_sharings et des documents des tiers. Ainsi un dossier sans client mais avec au moins un tiers affiche bien la vue détail et les onglets (dont « Tiers invités »). **Modifications :** - Fichier : `lecoffre-front-main/src/front/Components/Layouts/Folder/FolderInformation/hooks/useFolderData.ts` - Calcul de `hasThirdParties` via `safeArrayLength(folderWithRelations?.third_parties) > 0`. - Retour de `doesFolderHaveParticipants` étendu à `hasThirdParties ||` (en plus de hasCustomers, hasFolderSharings, hasThirdPartyDocuments). - Typage du folder étendu pour inclure `third_parties` dans le cast (aligné sur `folder_sharings`). **Impact :** Les dossiers créés sans client puis complétés par l'ajout d'un ou plusieurs tiers affichent correctement la section participants (Clients / Notaires invités / Tiers invités). Aucun impact sur les dossiers avec client(s) ou avec documents tiers existants. **Modalités de déploiement :** Déploiement standard frontend (build-and-deploy). **Modalités d'analyse :** En environnement notaire : créer un dossier sans client, ajouter un tiers depuis « Gérer les acteurs » ou la page dédiée ; rouvrir le dossier et vérifier que la vue détail s'affiche avec au moins l'onglet « Tiers invités » et le tiers listé. **Complément (backend, 2026-03-11) :** En test et production, l’ajout seul d’un tiers ne faisait toujours pas apparaître les onglets membres. **Cause :** La réponse GET /notary/folders/:uid est construite via `instanceToPlain` (class-transformer) puis `forceRelationsAndDeedIntoPlain` ; `third_parties` n’était pas inclus dans les relations forcées, donc absente de la réponse JSON. **Correction :** Dans `OfficeFoldersGetOneByUidHydrationHelper.ts`, ajout de `forceRelationIntoPlain` pour `third_parties` (comme pour `folder_sharings`), afin que la réponse contienne toujours `third_parties` lorsque l’include le demande. Après déploiement backend, les onglets membres s’affichent pour un dossier avec uniquement des tiers. ### Recherche confrères : peu de résultats malgré noms/prénoms dans l'annuaire **Problème :** À la saisie dans l’ajout d’un confrère, le filtre ne remonte que quelques comptes alors que les notaires ont bien leurs noms et prénoms dans l’annuaire / IdNot. **Cause :** Lorsque l’API Annuaire V2 n’est pas configurée (`ANNUAIRE_API_BASE_URL` / `IDNOT_API_KEY` absents de `system_configuration`), le backend utilise le **legacy IdNot**. Le fallback après `GET /persons?q=...` appelait `GET .../api/pp/v2/personnes?page=1&size=100` **sans paramètre de recherche** : seules les 100 premières personnes de l’API étaient récupérées, puis filtrées en mémoire par la requête. La recherche ne portait donc que sur un sous-ensemble fixe, d’où très peu de résultats. **Root cause :** Dans `IdNotDirectoryApiSearchHelper.searchInLegacyIdNot`, le fallback vers `api/pp/v2/personnes` ne passait pas la requête (prenom / nomUsuel) à l’API. **Correction :** Passer la requête à l’API comme pour l’Annuaire V2 : découper la chaîne en `prenom` et `nomUsuel` et appeler `api/pp/v2/personnes?prenom=...&nomUsuel=...&page=0&size=100`. La recherche est ainsi effectuée côté API (ou au moins sur un jeu pertinent) au lieu des 100 premières lignes. **Modifications :** - Fichier : `lecoffre-back-main/src/services/common/IdNotDirectoryService/helpers/IdNotDirectoryApiSearchHelper.ts` — fallback legacy : construction de l’URL avec `prenom` et `nomUsuel` dérivés de la requête, `page=0`, `size=100`. **Impact :** La recherche confrère en mode legacy IdNot renvoie davantage de résultats cohérents avec la saisie. Pour couvrir **tous les notaires de France**, configurer `ANNUAIRE_API_BASE_URL` et `IDNOT_API_KEY` dans `system_configuration` afin d’utiliser l’API Annuaire V2. **Modalités de déploiement :** Déploiement standard backend. **Modalités d'analyse :** Vérifier en logs `[IdNotDirectoryService] Étape 2 (API v2): X résultat(s) après filtre sur Y personne(s)` ; en cas d’API ne supportant pas prenom/nomUsuel, adapter ou conserver un fallback sans paramètres. **Évolution (2026-03-12) – Annuaire seul, pagination complète, alerte si non configuré :** La recherche confrères ne s’appuie plus sur la base du site (users/contacts/offices) ni sur les confrères déjà invités (folder_sharing). Source unique : API Annuaire (ANNUAIRE_API_BASE_URL + IDNOT_API_KEY). Pagination complète (lookup puis PP personnes, pages de 50 jusqu’à épuisement). Si l’annuaire n’est pas configuré : le backend renvoie `{ results: [], annuaireConfigured: false, message }` ; le front affiche un message d’alerte, sans fallback. Réponse API : `{ results, annuaireConfigured, message? }`. Plus de fallback legacy IdNot lorsque l’annuaire n’est pas configuré. Fichiers modifiés : IdNotDirectoryService, IdNotDirectoryApiSearchHelper, FolderSharingSearchHelper (back) ; SearchNotaries (types), useNotarySearch, SearchConfrereSection, ShareFolderModal (front). **Évolution (2026-03-12) – Invitation confrères : validation avant envoi email :** Les confrères ajoutés via « Ajouter » sur la page participants du dossier sont désormais mis en attente locale (comme les tiers). L'appel API `shareFolder` et l'envoi de l'email d'invitation ne sont effectués qu'au clic sur « Valider ». Si l'utilisateur clique « Annuler », aucun partage n'est créé et aucun email n'est envoyé. Les confrères en attente sont affichés avec « Sera invité lors de la validation » et peuvent être retirés avant validation. Fichiers modifiés : useConfreresManager, finalizeOperationsHelpers, useFinalizeHandler, ExistingConfreresList, ConfreresSection, SearchConfrereSection, StakeholdersContent, types. **Recherche confrères vide en test (ou tout env) :** L’annuaire **doit absolument être configuré** pour que la recherche confrères fonctionne. Si **ANNUAIRE_API_BASE_URL** ou **IDNOT_API_KEY** sont absents ou vides dans la table `system_configuration`, l’API `GET /api/v1/notary/search/notaries?q=...` renvoie **503 Service Unavailable** avec un corps `{ results: [], annuaireConfigured: false, message }` (pas de 200 avec résultats vides). Ces clés sont injectées depuis `.secrets//env-full--for-bdd-injection.txt` lors de l’étape **setSettings** du déploiement. Vérifier que ce fichier contient les lignes `ANNUAIRE_API_BASE_URL=...` et `IDNOT_API_KEY=...` (noms exacts), puis relancer setSettings sur la cible. Les logs backend indiquent les clés manquantes : `[IdNotDirectoryService] Annuaire non configuré: clé(s) manquante(s) dans system_configuration: ...`. **« Aucun notaire trouvé » alors que des notaires existent :** Si l’annuaire est configuré (pas de 503) mais la recherche renvoie 0 résultat pour des noms connus : (1) Consulter les logs backend sur la cible (service lecoffreio-backend@) : `🔍 [IdNotDirectoryService] API Annuaire V2 lookup...` indique l’URL appelée (sans la clé) ; `✅ ... lookup page 0: X entité(s), Y notaire(s) retenus` indique si l’API retourne des entités et combien passent le filtre (champs requis idNot, firstName, lastName). (2) Si « X entité(s), 0 notaire(s) retenus » : la réponse API ne contient pas les champs requis (uid/idnot, prenom, nomUsuel) sur les personnes ; le log `⚠️ Incomplete directory result ignored` et `⚠️ Lookup a retourné N entité(s) mais 0 notaire retenu` confirment ce cas. Adapter le parsing dans `IdNotDirectoryApiSearchHelper` (noms de champs selon la doc API Annuaire V2) ou vérifier le format de réponse de l’environnement (qual vs prod). (3) Si une erreur HTTP (4xx/5xx) apparaît : le log `⚠️ API Annuaire V2 lookup page 0 erreur: ... status=...` donne le code et éventuellement les clés du corps de réponse ; vérifier l’URL de base (ANNUAIRE_API_BASE_URL sans /api à la fin), la clé API et les droits du compte. (4) Fallback PP personnes : si le lookup échoue ou retourne 0 entité, le backend appelle `api/pp/v2/personnes?prenom=...&nomUsuel=...` ; vérifier en logs `API Annuaire V2 PP page 0: ... résultat(s) après filtre`. ### Secrets (.secrets) et copie vers les cibles Les répertoires `.secrets//` ne sont **pas dans git** (voir `.gitignore`). Ils doivent exister en local avant tout déploiement. Lors du déploiement (`deploy/scripts_v2/deploy.sh`), le script `sync-secrets-to-target.sh` copie les fichiers suivants vers la cible (test, pprod ou prod) dans `APP_ROOT/.secrets//` : - `.env.` (connexion SSH / config déploiement) - `.env..connectDB` (credentials BDD V2 et V1) - `env-full--for-bdd-injection.txt` (injection BDD / NEXT_PUBLIC_*) La base V1 est toujours distante : `DATABASE_V1_HOST` dans `.env..connectDB` ne doit **pas** être `localhost` ni `127.0.0.1`. En cas de valeur locale, `sync-secrets-to-target.sh` refuse la synchronisation (exit 1) et affiche une erreur. Le script est dans deploy/scripts_v2/remote/ ; PROJECT_ROOT doit être la racine du dépôt (SCRIPT_DIR/../../..) pour que le contrôle s'applique au bon fichier .secrets//.env..connectDB. backend.env n'existe plus sur les cibles : il est sauvegardé dans .secrets//backup/ puis supprimé à chaque sync ; la seule source pour la BDD est .env..connectDB. #### Phase 5 (check-v1-migration-status) : quel fichier est utilisé ? La Phase 5 s'exécute **sur la cible** (via SSH, heredoc dans `check-v1-migration-status.sh`). Elle lit **uniquement le fichier sur la cible** : `APP_ROOT/.secrets//.env..connectDB` (ex. `/srv/4NK/test.lecoffreio.4nkweb.com/.secrets/test/.env.test.connectDB`). Aucune lecture du fichier local pendant Phase 5. **Copie vers la cible au déploiement :** 1. **deploy.sh** appelle **sync-secrets-to-target.sh** : le fichier **local** `.secrets//.env..connectDB` est copié vers la cible `APP_ROOT/.secrets//`. Si le fichier local est **absent**, aucune copie n'est faite et la cible garde son fichier existant. 2. **import-v1.sh** au début recopie le connectDB local vers la cible (`deploy/` puis `cp` vers `.secrets//`). 3. **import-v1.sh** juste avant Phase 5 recopie à nouveau le connectDB local vers la cible `.secrets//`, afin que Phase 5 utilise exactement le même contenu que la machine locale (évite dérive si un script intermédiaire ou une action manuelle a modifié le fichier sur la cible). Si le fichier local est absent, sync-secrets et import-v1 ne copient rien ; Phase 5 lit alors l’ancien fichier déjà présent sur la cible (éventuellement incorrect). En cas d’erreur « password authentication failed » en Phase 5 alors que `run-verify-db-connection.sh` réussit plus tard : vérifier que le fichier **local** `.secrets//.env..connectDB` existe et contient le bon mot de passe, puis relancer le déploiement (la copie avant Phase 5 recalera la cible sur le fichier local). **Pourquoi Phase 5 a pu échouer alors que run-verify-db-connection réussissait :** Phase 5 chargeait le fichier avec une boucle manuelle dans un heredoc (`while read` + `export "$trimmed"`), alors que run-verify-db-connection utilise `_lib.sh` et `load_env_file_export`. Un mot de passe avec caractères spéciaux pouvait être tronqué ou modifié par la boucle. Depuis la correction, Phase 5 utilise uniquement la même logique (source `_lib.sh` puis `load_env_file_export`) ; pas de fallback — si `_lib.sh` est absent sur la cible, Phase 5 sort en erreur (exit 2) avec un message explicite pour comprendre la cause. **Correction du parsing dans `load_env_file_export` (_lib.sh) :** L’ancienne ligne `export "${key}=${value}"` provoquait l’expansion par le shell de `${value}` : un mot de passe contenant `$` ou `` ` `` était modifié (ex. `my$ecr3t` → `my` si la variable `ecr3t` est vide). La correction : exporter la valeur sans expansion en l’enveloppant en guillemets simples et en échappant les `'` dans la valeur (`escaped="${value//\'/\'\\\'\'}"` puis `export "${key}='${escaped}'"`). Ainsi les mots de passe avec `$`, `` ` ``, guillemets ou retours à la ligne sont transmis correctement à `psql`. Si le fichier `.env..connectDB` contient `ENV='test'` ou `ENV="test"`, la valeur doit être délivrée sans les guillemets pour que `apply-connectdb.sh` puisse comparer `ENV` à l’argument `env` (ex. `test`) ; une comparaison avec guillemets inclus provoquait « ENV mismatch: connectDB ENV='test' but requested env=test ». La suppression des guillemets en début et fin de valeur est faite par expansion de paramètre (premier et dernier caractère) dans `load_env_file_export`, de façon portable (sans regex contenant des guillemets). ### Échanges V1 / V2 : règles et flux Règles invariantes : **toute écriture en base cible V2 uniquement** ; **V1 strictement en lecture** lorsqu'elle est utilisée. #### Règle globale : écriture uniquement en V2 - L'application backend (repositories, services) utilise une seule connexion Prisma vers la base **V2** (DATABASE_*). Aucun code applicatif n'écrit en V1. - Les scripts deploy qui modifient la base utilisent `.env..connectDB` avec `load_connectdb_v2` pour les écritures ; les écritures (INSERT, UPDATE, DELETE, TRUNCATE) vont toujours vers la base V2 (locale à la cible). #### Sync au login (utilisateur qui se connecte) - **Contexte** : à la connexion d'un utilisateur, une synchronisation optionnelle (V1 vers V2) peut être déclenchée pour ses bureaux (offices). - **V1** : utilisée **uniquement en lecture** (SELECT) pour récupérer les données des offices de l'utilisateur et les lignes à fusionner. Fichiers : `V1ToV2LoginSyncService`, `V1ToV2MergeHelper` ; connexion via `getV1DatabaseConfig` (config lue depuis system_configuration / connectDB). - **V2** : toutes les écritures (upsert) sont faites via Prisma (`this.database.getClient()`, `prisma.$executeRawUnsafe`) vers la base V2 uniquement. L'ancrage après sync (`V1ToV2LoginSyncAnchoringHelper`) écrit aussi uniquement en V2. - **Portée (scope)** : le sync ne concerne **que l'utilisateur connecté** (notaire ou notaire invité). Il est déclenché uniquement depuis `finalizeUserLoginFlow` avec `ensuredPayload.userId` (utilisateur authentifié). Les offices sont dérivés de V1 via `user_office_affiliations WHERE user_uid = $1` ; toutes les tables mergées et l'ancrage sont filtrés par ces `officeUids`. Aucun autre utilisateur n'est concerné. | Élément | Lecture | Écriture | |--------|--------|----------| | Base V1 (Scaleway distante) | Oui (SELECT, scoped by user offices) | Non | | Base V2 (locale cible) | Oui | Oui (upsert, ancrage) | #### Import au déploiement (import-v1-data-direct-into-v2.sh) - **V1** : **lecture seule**. Connexion `psql_v1` / `pg_dump` vers DATABASE_V1_* ; uniquement SELECT et COPY TO STDOUT (export). Aucun INSERT/UPDATE/DELETE sur V1. - **V2** : **seule cible d'écriture**. `psql_admin` = `psql -d "${DATABASE_NAME}"` (base V2) pour TRUNCATE, INSERT, UPDATE, ANALYZE. Les bases temporaires (tmp_db, TEMP_DB) sont créées sur l'hôte local (même machine que V2), pas sur V1. En-tête du script : `# CRITICAL: V1 is READ-ONLY.` et `# All writes target V2 only. Never modify V1.` #### Autres scripts et fichiers | Fichier / zone | Usage V1 | Usage V2 | |----------------|----------|----------| | `verify-db-connection.sh` | Lecture (SELECT 1) via .env..connectDB DATABASE_V1_* | Lecture (SELECT 1) via DATABASE_* | | `run-verify-v1-connection-local.sh` | Lecture (SELECT 1) depuis .secrets//.env..connectDB | — | | `read-v1-contacts-emails.sh` | Lecture seule (structure, effectifs, stats emails) | — | | `recover-files-from-v1.ts` | Lecture (v1Prisma / --v1-database-url) pour importer documents/fichiers manquants | Écriture (prisma.create/update) vers V2 uniquement | | Scripts ensure-* / promote-* / seed-* (connectDB) | — | Lecture/écriture via load_connectdb_v2 ou connectDB V2 | #### backend.env - **Plus utilisé** : après backup dans `.secrets//backup/`, le fichier `backend.env` est supprimé sur chaque cible à chaque exécution de `sync-secrets-to-target.sh`. La seule source pour la BDD est `.env..connectDB` (blocs DATABASE_* et DATABASE_V1_*). #### systemd et .secrets/ - Les unités systemd (backend, frontend, router) n'utilisent plus `/etc/lecoffreio//backend.env`, `frontend.env` ni `router.env`. Elles chargent uniquement les fichiers de `APP_ROOT/.secrets//` via des drop-ins par instance : `.env.` pour tous les services, et `.env..connectDB` en plus pour le backend. Ces fichiers ne sont pas modifiés par le déploiement. Le fichier `.env.` doit contenir notamment : pour le backend `ENV`, `NODE_ENV`, `APP_PORT` ; pour le frontend `PORT` ; pour le router `ROUTER_LISTEN_HOST`, `ROUTER_LISTEN_PORT`, `BACKEND_UPSTREAM`, `FRONTEND_UPSTREAM`. ### Phase 5 (vérification migration) : échec authentification PostgreSQL **Problème :** Déploiement prod en échec en Phase 5 (code 2) : authentification PostgreSQL échouée pour le rôle (ex. `lecoffre-user-prod`) sur 127.0.0.1:5432. **Cause :** Phase 5 charge uniquement `.secrets//.env..connectDB` sur la cible. L'auth échoue si ce fichier est absent, ou si `DATABASE_PASSWORD` ne correspond pas au rôle PostgreSQL, ou si une variable est vide/mal parsée. **Cause racine :** Fichier connectDB absent ou non synchronisé avec les identifiants réels de la cible ; ou pas de contrôle/test de connexion avant les requêtes. **Correction :** La Phase 5 utilise uniquement `.secrets//.env..connectDB`. Après chargement : vérification des variables DATABASE_* ; sinon exit 2. Puis test de connexion psql ; en cas d'échec, message explicite et exit 2. Assurer que .secrets//.env..connectDB est rempli et synchronisé vers la cible (backend.env ne doit plus exister après migration). **À faire :** S'assurer que `.secrets//.env..connectDB` est rempli (V2 et V1 si import) et synchronisé vers la cible. Après migration, backend.env ne doit plus exister (sauvegarde puis suppression). En cas d'échec auth : aligner le mot de passe du rôle PostgreSQL avec DATABASE_PASSWORD du fichier connectDB. **Modalités d'analyse :** Exécuter `bash deploy/scripts_v2/check-v1-migration-status.sh prod` (ou test/pprod) ; en cas d'échec, le message indique le fichier chargé. Tester la connexion : `bash deploy/scripts_v2/run-verify-db-connection.sh prod`. ### Réancrage Phase 4 : erreur « Format tableur non supporté » traitée en skip **Problème :** Lors du réancrage (Phase 4), si le déchiffrement échoue (GCM) et que le validateur de format renvoie « Format tableur non supporté » (ou autre message de rejet de format), le document n'était pas considéré comme à ignorer et la phase pouvait échouer. **Correction :** Extension de `analyzeDecryptionError()` pour reconnaître tous les messages de rejet de format (`FileFormatValidator.getRejectionMessage()`) et marquer le document en skip. Rebuild backend puis relancer Phase 4 / déploiement si besoin. ### Réancrage Phase 4 (prod) : « PDF conversion failed » / « PDF invalide » traités en skip **Problème :** En prod, la Phase 4 (réancrage) s'arrêtait (exit 1) lorsque des documents notary provoquaient une erreur de déchiffrement (GCM) puis « PDF conversion failed: generated PDF is invalid » ou « PDF invalide après conversion ». Ces messages n'étaient pas reconnus comme skip, donc le document allait en échec et le script quittait avec code 1. **Correction :** Extension de `analyzeDecryptionError()` pour reconnaître « pdf conversion failed », « pdf invalide », « generated pdf is invalid », « failed to parse pdf » et marquer le document en skip. Rebuild backend puis redéployer / relancer Phase 4. Complément : si `anchorDocumentForReanchor` renvoyait bien `skipped: true`, `processDocumentForReanchor` ne propageait pas `anchorResult.skipped`/`reason` et le document était encore compté en failed (« Erreur inconnue »). Correctif : dans `processDocumentForReanchor`, traiter en premier `anchorResult.skipped && anchorResult.reason` et retourner `{ success: false, skipped: true, reason }` ; fallback `error: anchorResult.error ?? "Erreur inconnue"` (fichier `ReanchorHelpers.ts`). ### Réancrage Phase 4 : propagation de skipped/reason (processDocumentForReanchor) **Problème :** Même après reconnaissance des libellés PDF invalide / conversion échouée, le script affichait « Erreur document notary … : Erreur inconnue » et quittait en exit 1. **Cause :** `processDocumentForReanchor` ne testait que `anchorResult.error` ; lorsque `anchorDocumentForReanchor` retournait `{ success: false, skipped: true, reason }` (sans `error`), le retour générique était `{ success: false, skipped: false, error: undefined }`. **Correction :** Dans `ReanchorHelpers.ts`, si `!anchorResult.success` et `anchorResult.skipped && anchorResult.reason`, retourner immédiatement `{ success: false, skipped: true, reason: anchorResult.reason }` ; sinon utiliser `error: anchorResult.error ?? "Erreur inconnue"`. ### Réancrage Phase 4 (prod) : « V1 decrypt produced empty buffer » traité en skip **Problème :** En prod, la Phase 4 (réancrage) pouvait encore quitter avec exit 1 lorsque l'erreur était « [FilesNotaryService] V1 decrypt produced empty buffer for <uid> ». Ce message n'était pas reconnu par `analyzeDecryptionError`, donc le document était compté en failed. **Cause :** La fonction `analyzeDecryptionError` (ReanchorHelpers.ts) ne reconnaissait que « buffer is empty » et « buffer vide » dans le bloc « buffer vide », pas « empty buffer » ni « v1 decrypt produced empty buffer ». **Correction :** Dans le bloc « buffer vide » de `analyzeDecryptionError`, ajouter les conditions `normalizedMessage.includes("empty buffer")` et `normalizedMessage.includes("v1 decrypt produced empty buffer")` pour traiter ces erreurs en skip (fichier expiré / buffer vide). ### Migration V1 → V2 : `import-v1-direct` (MERGE) — collisions d’unicité sur `(name, office_uid)` **Problème :** Le mode `MERGE` peut échouer sur des tables dont l’unicité métier est portée par `(name, office_uid)` (ex. `deed_types`, `document_types`) lorsque des lignes V1 utilisent un `uid` différent d’une ligne déjà existante en V2 pour le même couple `(name, office_uid)`. **Cause :** Upsert basé sur `uid` (PK) alors que certaines tables ont aussi une contrainte unique sur `(name, office_uid)`. **Correction :** Avant l’UPSERT, appliquer un mécanisme générique piloté par allowlist : Optionnel : limiter l’import aux éléments postérieurs à une date (facile à modifier/enlever) via `deploy/import-v1-last-ok.txt` (fichier gitignoré). - Construire un mapping `uid_v1 → uid_v2` depuis la clé métier (ex. `(name, office_uid)`) et l’exporter en CSV dans `/tmp` - Réécrire ensuite les clés étrangères vers ces tables via discovery des FK (`pg_constraint`) pendant le run Détails : allowlist par table (deed_types, document_types, users idnot, office_folders folder_number), voir scripts import-v1-direct. ### [import-v1-direct] MERGE customers : FK `customers_contact_uid_fkey` **Problème :** Lors du MERGE, l'étape customers échoue car des lignes V1 ont un `contact_uid` absent de la table `contacts`. **Correction :** Ajout de la table `contacts` à la liste des tables sans filtre date, pour que tous les contacts V1 soient mergés avant les customers. Les customers sont tous insérés (pas de filtre). Vérification en lecture seule en V1 : `SELECT uid, created_at, updated_at FROM contacts WHERE uid = ''`. ### Suppression des synchronisations IdNot / Annuaire / Stripe (tables `sync_*` / `annu_*`) **Problème :** Les parcours de recherche / résolution de confrères (ex: par email) pouvaient dépendre de tables de synchronisation (`sync_*`, `annu_*`) et de scripts/endpoints de sync dédiés. **Cause :** Présence d’un mécanisme de synchronisation batch (IdNot/Annuaire/Stripe) avec tables dédiées et endpoints/scripts associés. **Cause racine :** Le besoin “référentiel complet” a été implémenté par réplication locale (tables sync) au lieu d’un usage systématique des appels API au moment de la demande. **Correction :** - Suppression des points d’entrée / scripts / endpoints de sync et retrait des modèles Prisma `sync_*` / `annu_*` / `sync_stripe_subscriptions`. - Les recherches utilisent désormais des **appels API** (IdNot/Annuaire) au moment de la demande, avec complément base du site et `folder_sharing`. **Impact :** - Plus d’usage applicatif des tables de synchronisation. - Les réponses reposent sur la base réelle (users/contacts/offices) et les APIs externes au moment du besoin. **Modalités de déploiement :** Déploiement standard backend. **Modalités d'analyse :** - Vérifier la recherche confrères (partage de dossier) et la résolution par email (logs backend, réponses API). ### Super-admin : chargement matrice de droits à la demande **Problème :** Le frontend chargeait systématiquement `GET /api/v1/super-admin/role-permissions` dès qu’un utilisateur était identifié comme `super-admin`, y compris sur des pages non liées (ex: `/folders`). En cas d’aléa réseau, cela générait des erreurs `Failed to fetch` et du bruit console, sans valeur fonctionnelle. **Cause :** Le refresh global des permissions déclenchait un chargement complet de matrice dès `permissionContext.globalRole === "super-admin"`. **Cause racine :** Le chargement de la matrice était implémenté comme un effet global plutôt que comme un besoin local à l’écran d’administration de matrice, ce qui augmente la surface réseau et les points de défaillance. **Correction :** - Le refresh global ne charge plus la matrice complète pour les super-admins. - La matrice complète est chargée uniquement sur la page `/super-admin/role-permissions` (à la demande). - Suppression des imports dynamiques (`import()`/`require()`) sur les modules RolePermissions via extraction des types. **Impact :** - Plus d’erreur réseau sur `GET /super-admin/role-permissions` lors de navigation standard. - Réduction des dépendances dynamiques côté front sur ce module. **Modalités de déploiement :** Déploiement standard frontend. **Modalités d’analyse :** - Se connecter en super-admin et naviguer vers `/folders` puis vérifier l’absence d’appel `GET /api/v1/super-admin/role-permissions`. - Naviguer vers `/super-admin/role-permissions` et vérifier que l’appel est déclenché (si la page existe). ### Refactoring Linting Frontend (Transition ESLint 250/40) **Objectif :** Réduire la dette technique et améliorer la maintenabilité du code frontend en respectant les règles ESLint strictes (max 40 lignes par fonction, max 250 lignes par fichier, max 4 paramètres). **Modifications :** - **BaseApiService :** Refactoring pour utiliser `RequestOptions` et `ProcessResponseOptions` afin de réduire le nombre de paramètres. - **Factories :** Découpage des factories API (Auth, Admin, Notary, Customer) en extrayant les méthodes dans des helpers dédiés (`*MethodsHelpers.ts`). - **Helpers :** Création de nombreux helpers pour alléger les fichiers principaux. - **Imports :** Utilisation d'alias `@Front/Api/types` pour simplifier les imports. **Impact :** - Code plus modulaire et plus facile à tester. - Réduction significative des erreurs de lint `max-lines-per-function`, `max-params` et `complexity`. - Pas de changement fonctionnel (refactoring pur). **Modalités de déploiement :** Déploiement standard du frontend. **Modalités d'analyse :** Vérifier que l'application fonctionne correctement (navigation, appels API). `npm run lint` doit rapporter moins d'erreurs. ### Correction de la visibilité des dossiers archivés pour les clients **Problème :** Les clients pouvaient voir les dossiers archivés dans leur liste de dossiers, ce qui n'est pas le comportement attendu. **Cause :** Le filtre `status: "LIVE"` était manquant dans la configuration de la requête Prisma pour les clients dans `OfficeFoldersController`. **Correction :** Ajout explicite du filtre `status: "LIVE"` et `deleted_at: null` dans les méthodes de configuration de requête pour les clients et les tiers. **Modifications :** - Fichier : `lecoffre-back-main/src/app/api/customer/OfficeFoldersController.ts` - Ajout de `status: "LIVE"` dans `configureThirdPartyQuery` et `configureCustomerQuery`. - Ajout de la vérification du statut dans `getOneByUid`. **Impact :** Les clients et tiers ne verront plus les dossiers archivés ou supprimés dans leur interface. **Modalités de déploiement :** Déploiement standard du backend. **Modalités d'analyse :** Vérifier que les dossiers ayant le statut `ARCHIVED` ou `DELETED` n'apparaissent plus dans la liste des dossiers côté client. ### Correction de la génération de ZIP avec fichiers multiples (collision de noms) **Problème :** Lors de la sélection de plusieurs documents pour créer un ZIP, si les noms de fichiers générés étaient identiques (ex: même type de document pour le même dossier), un seul fichier était présent dans le ZIP final car ils s'écrasaient mutuellement. **Cause :** Absence de gestion des collisions de noms de fichiers dans `ZipService.ts`. Le service ajoutait les fichiers à l'archive avec le nom généré par `FileNamingService`, sans vérifier si ce nom était déjà utilisé dans l'archive en cours de construction. **Correction :** Ajout d'une logique pour détecter les noms de fichiers déjà utilisés dans l'archive et ajouter un suffixe incrémental `(n)` pour les rendre uniques. La logique a été factorisée dans `FileNamingService.getUniqueFileName`. **Modifications :** - Fichier : `lecoffre-back-main/src/services/common/ZipService/ZipService.ts` - Utilisation de `FileNamingService.getUniqueFileName` pour garantir l'unicité des noms dans l'archive. - Fichier : `lecoffre-back-main/src/services/common/FileNamingService/FileNamingService.ts` - Ajout de la méthode `getUniqueFileName` pour gérer la déduplication avec suffixe `(n)`. **Impact :** Les ZIPs générés contiendront désormais tous les fichiers sélectionnés, même s'ils ont le même nom de base. **Modalités de déploiement :** Déploiement standard du backend. **Modalités d'analyse :** Vérifier que les ZIPs contenant plusieurs fichiers avec le même nom (ex: deux "Pièce d'identité" dans le même dossier) contiennent bien tous les fichiers avec des noms distincts. ### Correction des permissions V2 pour le rôle `admin` (matrice des droits) **Problème :** Un utilisateur d’étude avec `global_role = admin` pouvait se retrouver avec une matrice V2 très restreinte côté UI (pages/actions manquantes sur documents/fichiers/partages/rappels/demandes), tandis que les collègues résolus en `collaborator` avaient le périmètre complet. **Cause :** La V2 filtre la matrice de permissions sur le rôle résolu. Pour un utilisateur `admin`, la résolution conserve `admin`, donc l’UI dépend directement des entrées `admin` présentes dans `role_permissions_matrix`. **Cause racine :** Les scripts/migrations existants assuraient la complétude de `collaborator` sur les ressources notariales, mais pouvaient laisser `admin` incomplet (notamment via certains scripts d’ensure). **Correction :** Backfill idempotent (insert-only) des entrées `admin` à partir des entrées `collaborator` pour les ressources notariales : - Script d’ensure : `deploy/scripts_v2/remote/sql/ensure-role-permissions-matrix-folders.sql` (clonage `collaborator` → `admin` avec `ON CONFLICT DO NOTHING`). - Migration Prisma : `20260305120000_backfill_admin_permissions_from_collaborator`. - Ensure au login : lors de la génération du JWT pour un utilisateur `admin`, backfill idempotent `collaborator` → `admin` si la matrice est incomplète (avec reload du cache). **Modalités de déploiement :** Déploiement standard du backend (migrations via `prisma migrate deploy`). En cas de base restaurée / matrice incomplète, exécuter le script d’ensure habituel. - Depuis `deploy/scripts_v2/deploy.sh` : exécution systématique de `deploy/scripts_v2/remote/ensure-role-permissions-matrix-folders.sh` après migrations. - Diagnostics (logs) : - `deploy/scripts_v2/run-check-user-offices-logs.sh ` : journalctl backend centré sur `UserOffices` et sync offices (utile quand le front loggue `OfficeSelector hidden: no offices available`). - Les scripts de diagnostic `scripts_v2/remote/*` utilisent `print_journalctl_matches` / `count_journalctl_matches` (définis dans `deploy/scripts_v2/remote/_lib.sh`) pour rester stables avec `set -euo pipefail` même quand il n’y a aucun match. **Modalités d'analyse :** Contrôler que `role_permissions_matrix` contient des entrées `admin` sur `documents`, `files`, `folder_customer`, `folder_third_parties`, `folder_sharing`, `document_reminder`, `document_request`, et que `GET /api/v1/notary/my-permissions` pour un utilisateur `admin` renvoie ces ressources/pages. ### Correction : offices sans `office_roles` + Annuaire (champs requis) **Problème :** - Certains offices peuvent exister sans aucune entrée dans `office_roles` (absence de `Notaire` / `Collaborateur`), ce qui peut dégrader le calcul des règles au login. - Les résultats Annuaire/IdNot peuvent contenir des personnes incomplètes (sans `idNot`, `firstName`, `lastName`) qui ne doivent pas être proposées comme ajoutables. **Cause :** - Incohérences de données historiques / import / migrations. - Qualité variable des données source Annuaire. **Correction :** - Vérification/correction au login : ensure idempotent des rôles d’office par défaut (`Notaire`, `Collaborateur`) et liaison aux règles attendues. - Vérification/correction systématique au déploiement : exécution d’un ensure SQL idempotent via `deploy/scripts_v2/deploy.sh`. - Filtrage Annuaire : exclusion + log des résultats incomplets (champs requis). **Modifications :** - `lecoffre-back-main/src/services/common/IdNotService/IdNotRoleService.ts` - `lecoffre-back-main/src/services/common/AuthService/helpers/JwtPayloadGenerationHelper.ts` - `lecoffre-back-main/src/services/common/IdNotDirectoryService/helpers/DirectoryRequiredFieldsHelper.ts` - `deploy/scripts_v2/remote/sql/ensure-default-office-roles.sql` - `deploy/scripts_v2/deploy.sh` - Au démarrage : `git add -A` + commit + push local (si changements) AVANT toute action distante. - Sur la machine cible : reset/checkout du ref via `git checkout -B /` pendant le sync. **Modalités de déploiement :** - Déploiement standard V2. - En cas de base restaurée : `deploy/scripts_v2/run-ensure-default-office-roles.sh `. **Modalités d'analyse :** ### Vérification migration i18n / site texts (backend, frontend, ressources) **Objectif :** Vérifier que les textes utilisateur sont migrés en i18n (site texts) et configurés dans `.secrets//seed-site-texts-.ts` et `.secrets//env-full--for-bdd-injection.txt` pour chaque environnement. **État constaté :** - **Frontend :** Les écrans Login, MyAccount, et la page Office (informations) utilisent `useSiteTextContent` avec des clés déclarées dans le seed. Les clés utilisées (65 à 80 selon les écrans) sont présentes dans le seed et dans `SITE_TEXTS_KEYS_INDEX` de l’env-full. Le boot (page `/_app`) précharge désormais aussi le scope `components` (en plus de `login`) via `lecoffre-front-main/src/front/Stores/siteTextsBootConfig.ts`, pour éviter les logs `Missing site text` quand une clé `components.*` est en base mais pas chargée. Plusieurs écrans (dossiers, formulaires tiers, super-admin, etc.) contiennent encore des libellés en dur ; une migration progressive peut étendre le scope `components` ou ajouter des scopes métier et les clés correspondantes au seed puis à l’env-full. - **Backend :** Les messages d’erreur et de validation sont centralisés dans `lecoffre-back-main/src/common/utils/errorMessages.ts`. Les réponses API (erreurs 4xx/5xx) utilisent ces constantes. Quelques messages de succès sont en dur dans les contrôleurs (ex. `IncidentReportController`, `FolderSharingResendHelper`, `FolderThirdPartiesResendCodeHelper`, `DocumentsGuestNotaryHelper`). Aucune résolution de clés site texts côté backend pour les réponses API n’est en place ; une évolution ultérieure pourrait exposer des clés i18n côté API si besoin. - **Ressources :** Le package `le-coffre-resources` fournit des types (Notary, SuperAdmin, etc.) ; pas de chaînes utilisateur à migrer. **Modifications effectuées :** - **MyAccount / Mon compte :** Utilisation des clés `myAccount.office.idNot.label` et `myAccount.office.idNot.placeholder` déjà présentes dans le seed ; passage de ces libellés depuis le parent vers `MyAccountOfficeSection` (plus de texte en dur pour l’identifiant ID.NOT). - **Page Office (informations) :** Ajout de 11 clés dans le seed (scope `office`) : `office.labels.crpcen`, `office.labels.denomination`, `office.labels.address`, `office.titles.administrators`, `office.titles.collaborators`, `office.userLabels.lastName`, `office.userLabels.firstName`, `office.userLabels.email`, `office.userLabels.phone`, `office.userLabels.role`, `office.header.active`. Mise à jour de `.secrets/test/seed-site-texts-test.ts` et de `.secrets/test/env-full-test-for-bdd-injection.txt` (SITE_TEXTS_KEYS_INDEX). Page `offices/[officeUid]` enveloppée dans `SiteTextsProvider` avec `scopes={["office"]}` ; composant `OfficeInformations` migré pour utiliser `useSiteTextContent` et passer les libellés à la vue (correction orthographique « Addresse » → « Adresse » via la clé). **Modalités de déploiement :** - Exécuter le seed site texts après déploiement si les nouvelles clés doivent être présentes en base : `npm run site-texts:seed` (backend) en s’assurant que le fichier seed et l’env-full sont bien déployés (voir `deploy/scripts_v2`). - Pour pprod/prod : reproduire les entrées du seed test dans les dépôts concernés (`.secrets//seed-site-texts-.ts` et `env-full--for-bdd-injection.txt`) puis exécuter le seed sur l’environnement cible. **Modalités d’analyse :** - Vérifier que la page Mon compte affiche bien les libellés ID.NOT depuis le seed. - Vérifier que la page Office (super-admin) affiche les libellés CRPCEN, Dénomination, Adresse, Administrateurs, Collaborateurs, Nom, Prénom, E-mail, Téléphone, Rôle depuis le seed (scope `office`). - Vérifier que `SITE_TEXTS_KEYS_INDEX` dans l’env-full contient toutes les clés du seed (ordre identique recommandé). - Vérifier au login que `officeRole` est résoluble et que les `rules` associées sont chargées. - Vérifier que les résultats Annuaire incomplets sont ignorés et loggués (missing fields + `idNotPrefix`). ## Évolutions documentées ### Centralisation vérification accès invité notaire **Date** : 2026-03-02 #### Objectif Centraliser la logique « vérifier accès invité notary » (email invité + folder + hasAccess via folder_sharing) dans un helper générique, utilisé par le téléchargement batch (FilesBatchDownloadGuestNotaryHelper) et le téléchargement single fichier notary (FilesNotaryDownloadGuestNotaryHelper). La logique batch ne sert plus qu’au regroupement d’emails ou à la vérification des relances, pas pour l’upload de documents. #### Impacts - Un seul point de vérité pour : résolution de l’email invité (getEffectiveGuestNotaryEmail), appel folderSharingService.hasAccess(folderUid, "GUEST_FOLDERS", email, idNot), et réponse 404 en cas d’échec. - Réduction de la duplication entre FilesBatchDownloadGuestNotaryHelper et FilesNotaryDownloadGuestNotaryHelper. - Signature commune : verifyGuestNotaryAccess({ permissionContext, folderUid, req, response, operationLabel, logContext, contextId }) → { hasAccess: true, folderUid } | { hasAccess: false }. #### Modifications - **Création** : `lecoffre-back-main/src/app/api/common/helpers/GuestNotaryAccessVerificationHelper.ts` — exporte `verifyGuestNotaryAccess` et types associés. - **Refactor** : `FilesBatchDownloadGuestNotaryHelper.ts` — résolution du folderUid (existant), puis appel au helper commun ; suppression de la duplication email + hasAccess. - **Refactor** : `FilesNotaryDownloadGuestNotaryHelper.ts` — après verifyDocumentAndFolder, appel au helper commun ; suppression de la fonction locale verifyGuestNotaryAccess (renommée en checkGuestNotaryAccess qui délègue au helper commun). Suppression du paramètre userEmail de handleGuestNotaryFileFromFiles (l’email est résolu dans le helper commun via permissionContext). - **Appelant** : `FilesNotaryDownloadHelper.ts` — appel à handleGuestNotaryFileFromFiles sans userEmail ; extraction simplifiée (isGuestNotary uniquement si besoin). #### Modalités de déploiement Déploiement standard (build-and-deploy.sh). Aucune migration ni variable d’environnement. #### Modalités d’analyse - Téléchargement single fichier notary en notaire invité : accès autorisé uniquement si l’invité a accès au dossier via folder_sharing (GUEST_FOLDERS). - Téléchargement batch en notaire invité : même règle ; le folderUid est résolu à partir du premier fichier du batch. ### Remédiation : Sièges pour affiliations actives **Date** : 2026-03-04 **Auteur** : Équipe 4NK **Statut** : Implémenté #### Objectif Assurer que tout utilisateur avec une affiliation **ACTIVE** à un office disposant d’un abonnement **ACTIVE** possède bien un siège (licence) pour cet office. Cette vérification est effectuée : 1. **À la connexion (login)** : De manière automatique et non bloquante. 2. **Via script batch** : Pour remédiation de masse. #### Modifications ##### 1. Backend - Services * **`UserOfficeAffiliationsService`** : Ajout de la méthode `ensureSeatsForUserActiveAffiliations(userUid)`. * Récupère les affiliations actives. * Pour chaque affiliation, vérifie/crée le siège via `SubscriptionsService`. * **`SubscriptionsService`** : Modification de `isUserSubscribed`. * Pour les abonnements **UNLIMITED**, si le siège n'existe pas, il est créé automatiquement à la volée. * Pour les abonnements **STANDARD**, le comportement reste une simple vérification (retourne `false` si pas de siège). ##### 2. Backend - Flux de connexion * **`UserControllerLoginFlowAuthHelper`** : * Appel de `ensureSeatsForUserActiveAffiliations` après la synchronisation IdNot. * Exécuté dans un `try/catch` pour ne pas bloquer le login en cas d'erreur technique. ##### 3. Scripts * **Analyse SQL** : `scripts/analyze-seats-vs-affiliations.sql` * Liste les utilisateurs avec affiliation ACTIVE mais sans siège dans un office avec abonnement ACTIVE. * **Remédiation Batch** : `src/scripts/ensure-seats-for-active-affiliations.ts` * Parcourt tous les utilisateurs et lance la vérification `ensureSeatsForUserActiveAffiliations`. #### Modalités de déploiement 1. **Analyse préalable (Obligatoire)** : Exécuter le script SQL pour identifier les écarts avant correction. ```bash psql "$DATABASE_URL" -f scripts/analyze-seats-vs-affiliations.sql ``` 2. **Exécution du script de remédiation** : ```bash # Dry-run (simulation) npm run ensure:seats -- --dry-run # Exécution réelle npm run ensure:seats ``` 3. **Analyse post-remédiation (Obligatoire)** : Ré-exécuter le script SQL pour confirmer la résolution des écarts. ```bash psql "$DATABASE_URL" -f scripts/analyze-seats-vs-affiliations.sql ``` #### Modalités d'analyse * Vérifier les logs avec `[ensureSeats]` ou `[SubscriptionsService]`. * Utiliser le script SQL d'analyse pour vérifier qu'il n'y a plus d'écarts. ## Correctifs additionnels documentés ### Correctifs espace notaire invité (retour, téléchargement, note dossier) **Date** : 2026-03-02 #### Problèmes 1. **Lien retour après visualisation d’un document envoyé** Depuis la page « Documents envoyés par le notaire », clic sur un document puis sur Retour renvoyait vers la home (/folders) au lieu de la liste des documents envoyés. 2. **Téléchargement de fichier avec accent** Télécharger un fichier (filigrane + métadonnées) dont le nom contient des accents pouvait mal s’afficher ou mal se nommer (Content-Disposition sans guillemets autour du filename). 3. **Note à la création du dossier** La note saisie à la création du dossier ne s’affichait pas sur la page « Préparer le dossier » (add/clients) après redirection ; elle n’apparaissait qu’après modification ultérieure sur la fiche dossier. #### Cause / root cause 1. **Retour** L’URL de retour dans ViewDocuments (Folder) est calculée via `useReceivedDocumentBackUrl`. Pour le notaire invité, elle dépend de `document.shared_to_office` et du statut SENT/DOWNLOADED. Lorsque le document est chargé via l’API DocumentsNotary (customer), `shared_to_office` n’est pas inclus dans la réponse, donc la condition échouait et on retombait sur l’URL par défaut (FolderInformation ou vide → home). Root cause : pas de fallback quand l’utilisateur est notaire invité avec un folderUid connu. 2. **Téléchargement** Dans `FilesNotaryDownloadHelper`, le header Content-Disposition utilisait `filename=${encodeURIComponent(...)}` sans guillemets. La RFC 2183 recommande une valeur entre guillemets pour les noms avec caractères spéciaux/accents. De plus, le nom utilisé était uniquement `file_name` au lieu de privilégier `display_name` quand disponible. 3. **Note** La note (description) est bien envoyée à la création et persistée en base. La page add/clients charge le dossier via GET /notary/folders/:uid mais ne mettait dans `folderMetadata` que `number` et `name`, pas `description`, et n’affichait nulle part la note sur « Préparer le dossier ». #### Correctifs 1. **Retour (viewDocumentsBackUrlHelpers.ts)** - Ajout d’un fallback : si l’utilisateur est notaire invité et que `folderUid` est défini, l’URL de retour est toujours `InvitedReceivedDocuments` (liste des documents envoyés), même lorsque le document n’a pas encore `shared_to_office` dans la réponse (ex. chargement via DocumentsNotary API). 2. **Téléchargement (FilesNotaryDownloadHelper.ts)** - Utilisation de `display_name` si présent, sinon `file_name`. - Content-Disposition : `filename="${encodeURIComponent(fileName)}"` (guillemets autour de la valeur). 3. **Note (AddClientToFolder)** - `FolderMetadata` étendu avec `description?: string | null`. - Dans `folderPageDataLoaderHelpers`, passage de `description` depuis le dossier chargé à `setFolderMetadata`. - Sur la page add/clients, affichage d’un bloc « Note du dossier » sous le sous-titre lorsque `folderMetadata.description` est renseigné (style `.folder-note` dans `classes.module.scss`). #### Modifications - `lecoffre-front-main/src/front/Components/Layouts/Folder/ViewDocuments/helpers/viewDocumentsBackUrlHelpers.ts` - `lecoffre-back-main/src/app/api/notary/helpers/FilesNotaryDownloadHelper.ts` - `lecoffre-front-main/src/front/Components/Layouts/Folder/AddClientToFolder/types.ts` - `lecoffre-front-main/src/front/Components/Layouts/Folder/AddClientToFolder/hooks/helpers/folderPageDataLoaderHelpers.ts` - `lecoffre-front-main/src/front/Components/Layouts/Folder/AddClientToFolder/index.tsx` - `lecoffre-front-main/src/front/Components/Layouts/Folder/AddClientToFolder/classes.module.scss` #### Environnements Les correctifs sont côté code (front + back) et s’appliquent à tous les environnements (test, pprod, prod) après déploiement. Aucune configuration spécifique par environnement. #### Modalités de déploiement Déploiement standard (build-and-deploy.sh). Aucune migration ni variable d’environnement. #### Modalités d’analyse - **Retour** : se connecter en notaire invité, ouvrir un dossier partagé, aller dans « Documents envoyés par le notaire », ouvrir un document, cliquer sur Retour → la page doit être la liste des documents envoyés, pas la home. - **Téléchargement** : en notaire invité (ou notaire), télécharger un document dont le fichier a un nom avec accents (et si possible filigrane) → le nom du fichier téléchargé doit être correct et lisible. - **Note** : créer un dossier avec une note « Note de dossier » renseignée, valider → après redirection sur « Préparer le dossier », la note doit apparaître sous le sous-titre. ### Problème : Note de dossier non sauvegardée à la création #### Description du problème Lors de la création d'un dossier, la note saisie dans le champ "Note de dossier" n'était pas sauvegardée. Le champ apparaissait vide après la création. #### Cause Le champ `description` (qui contient la note) dans la classe `OfficeFolder` (ressource Notary) n'avait pas de décorateur de validation pour le groupe `createFolder`. Lors de la validation de l'entité avec `class-validator` et l'option `whitelist: true`, les propriétés sans décorateur de validation actif pour le groupe donné sont supprimées de l'objet. Comme `description` n'avait que `@IsOptional({ groups: ["updateFolder"] })`, il était ignoré et supprimé lors de la validation avec le groupe `createFolder`. #### Correction Ajout du groupe `createFolder` au décorateur `@IsOptional` pour les champs `description`, `archived_description` et `status` dans `lecoffre-ressources-dev/src/Notary/OfficeFolder.ts`. ```typescript @Expose() @IsOptional({ groups: ["createFolder", "updateFolder"] }) public override description: string | null = null; @Expose() @IsOptional({ groups: ["createFolder", "updateFolder"] }) public override archived_description: string | null = null; @Expose() @IsOptional({ groups: ["createFolder", "updateFolder"] }) public override status: EFolderStatus | string = EFolderStatus.LIVE; ``` #### Impacts - La note de dossier est maintenant correctement sauvegardée à la création. - `archived_description` et `status` sont également correctement traités s'ils sont fournis à la création (bien que `status` ait une valeur par défaut). #### Modalités de déploiement - Recompiler `lecoffre-ressources-dev`. - Redéployer le backend pour prendre en compte la nouvelle version de la ressource. ### Correction V2 : Injection de dépendance circulaire dans OfficesService (ESM) **Date :** 04/03/2026 **Auteur :** Équipe 4NK #### Problème Malgré le fix précédent (lazy loading via getter), une erreur persistait lors de l'exécution sur l'environnement de test (ESM) : ```text TypeError: this.officesRibService.uploadEncryptedRibFromFiles is not a function ``` Cela indique que `OfficesRibService` est toujours injecté comme un objet vide `{}` à cause de la dépendance circulaire, même avec le getter, car l'import statique `import OfficesRibService from "./OfficesRibService"` est évalué au chargement du module et résolu en `{}` si le cycle n'est pas résolu. #### Analyse - **Cause racine :** Dépendance circulaire persistante. Dans un environnement ESM, si un module est importé avant d'être complètement évalué (cycle), l'import par défaut peut être un objet vide ou incomplet. - **Échec du fix précédent :** Le getter utilisait `Container.get(OfficesRibService)`, mais `OfficesRibService` (le symbole importé) était déjà `{}`. #### Solution Utilisation de l'import dynamique `await import(...)` à l'intérieur des méthodes pour garantir que le module est complètement chargé avant utilisation. ##### Modifications 1. Remplacement des imports statiques par `import type`. 2. Remplacement des getters synchrones par des méthodes privées asynchrones `getOfficesRibService()` et `getOfficesAnchorService()`. 3. Utilisation de `await import("./OfficesRibService")` pour récupérer la classe au runtime. 4. Mise à jour de toutes les méthodes appelantes pour être `async` (elles l'étaient déjà) et utiliser `await this.getOfficesRibService()`. ```typescript // Avant import OfficesRibService from "./OfficesRibService"; // ... private get officesRibService(): OfficesRibService { return Container.get(OfficesRibService); } // Après import type OfficesRibService from "./OfficesRibService"; // ... private async getOfficesRibService(): Promise { const { default: OfficesRibServiceClass } = await import("./OfficesRibService"); return Container.get(OfficesRibServiceClass); } ``` #### Vérification - Le code compile. - L'import dynamique casse le cycle de dépendance au niveau du chargement des modules. ### Correction : Injection de dépendance circulaire dans OfficesService **Date :** 04/03/2026 **Auteur :** Équipe 4NK #### Problème Le démarrage du backend échouait avec l'erreur suivante : ```text Error: OfficesRibService injected but missing uploadEncryptedRibFromFiles method. Injected type: Object ``` Cela indiquait que `OfficesRibService` était injecté comme un objet vide `{}` au lieu de l'instance de la classe, symptôme classique d'une dépendance circulaire mal gérée par le système d'injection de dépendances (TypeDI) lors de l'initialisation. #### Analyse - **Cause racine :** Dépendance circulaire entre `OfficesService` et `OfficesRibService` (probablement via `OfficesRibProcessingHelper` ou d'autres dépendances partagées). - **Impact :** Le service `OfficesService` tentait d'accéder à `OfficesRibService` dans son constructeur alors que ce dernier n'était pas encore totalement initialisé. #### Solution Mise en place du **Lazy Loading** pour les services problématiques dans `OfficesService`. ##### Modifications 1. Suppression de `officesRibService` et `officesAnchorService` du constructeur de `OfficesService`. 2. Utilisation de getters pour récupérer ces services via `Container.get()` uniquement au moment de leur utilisation. ```typescript // Avant constructor(private officesRibService: OfficesRibService) { ... } // Après private get officesRibService(): OfficesRibService { return Container.get(OfficesRibService); } ``` #### Vérification - Le code compile et les linters passent. - Cette approche est déjà utilisée dans `WatermarkService` pour résoudre des problèmes similaires. ### Correction : Extension manquante dans l'import dynamique de OfficesService **Date :** 2026-03-04 **Auteur :** Équipe 4NK #### Problème Une erreur 500 survenait lors du téléchargement du RIB : `Cannot find module '.../services/notary/OfficesService/OfficesService.js'` Cette erreur indiquait que Node.js n'arrivait pas à charger le module `OfficesService`, probablement à cause d'une erreur d'import interne. #### Cause racine Le fichier `OfficesService.ts` utilisait des imports dynamiques relatifs (`await import("./OfficesRibService")`) sans spécifier l'extension `.js`. Dans un environnement Node.js ESM (configuré avec `"type": "module"`), les imports relatifs doivent obligatoirement inclure l'extension du fichier. #### Correctif Ajout de l'extension `.js` aux imports dynamiques dans `src/services/notary/OfficesService/OfficesService.ts` : ```typescript // Avant const { default: OfficesRibServiceClass } = await import("./OfficesRibService"); const { default: OfficesAnchorServiceClass } = await import("./OfficesAnchorService"); // Après const { default: OfficesRibServiceClass } = await import("./OfficesRibService.js"); const { default: OfficesAnchorServiceClass } = await import("./OfficesAnchorService.js"); ``` #### Impact - `src/services/notary/OfficesService/OfficesService.ts` #### Vérification Le déploiement est nécessaire pour vérifier la correction. L'erreur de téléchargement du RIB devrait disparaître. #### Autre correction incluse Ajout de la clé de traduction manquante `components.dropdown.defaultAriaLabel` dans `.secrets/test/seed-site-texts-test.ts` pour corriger l'erreur frontend : `[Front] 🚫 [SiteTextsStore] Missing site text {textKey: 'components.dropdown.defaultAriaLabel', locale: 'fr-FR'}` ### Fix: Erreur téléchargement ZIP "files-notary" (Notaire invité) #### Problème Lorsqu'un notaire invité tente de télécharger plusieurs documents (ZIP) depuis la vue "Documents envoyés par le notaire", une erreur survient et l'utilisateur est parfois déconnecté. Le frontend appelle l'endpoint `POST /api/v1/notary/files-notary/download-multiple`, mais celui-ci n'était pas implémenté dans le contrôleur `FilesNotaryController`. #### Cause Racine L'endpoint `downloadMultiple` manquait dans `FilesNotaryController`. Bien que le helper `FilesBatchDownloadHelper` existe, il était câblé uniquement pour les fichiers standards (`FilesService`) et utilisé seulement par `FilesController`. #### Correctifs 1. **Modification de `FilesBatchDownloadHelper` et `FilesBatchDownloadZipHelper`** : * Ajout du paramètre `isNotaryFiles` (booléen, défaut `false`) pour indiquer si le téléchargement concerne des fichiers notaire (`FilesNotaryService`) ou standards. * Propagation de ce paramètre jusqu'à `ZipService.createZipFromFiles` qui supportait déjà cette option. 2. **Mise à jour de `FilesNotaryController`** : * Injection de `ZipService`. * Instanciation de `FilesBatchDownloadHelper`. * Ajout de la méthode `downloadMultiple` exposant la route `POST /api/v1/notary/files-notary/download-multiple` et appelant le helper avec `isNotaryFiles: true`. #### Impacts - `lecoffre-back-main/src/app/api/notary/helpers/FilesBatchDownloadHelper.ts` - `lecoffre-back-main/src/app/api/notary/helpers/FilesBatchDownloadZipHelper.ts` - `lecoffre-back-main/src/app/api/notary/FilesNotaryController.ts` #### Modalités de déploiement Déploiement standard du backend. #### Modalités d'analyse Vérifier le téléchargement multiple (ZIP) en tant que notaire invité sur des documents partagés par le notaire principal. ### Correction de l'avertissement de préchargement de police non utilisé et migration next/font **Date :** 04/03/2026 **Auteur :** Équipe 4NK #### Problème L'application affichait un avertissement dans la console du navigateur : `La ressource à l’adresse « ... » préchargée avec le préchargement de lien n’a pas été utilisée après quelques secondes.` De plus, certaines parties de l'application utilisaient encore des définitions de polices en dur ("Poppins", "Inter") au lieu d'utiliser le système optimisé de Next.js (`next/font`). #### Cause L'application utilise `next/font/google` pour charger les polices "Inter" et "Poppins" de manière optimisée. `next/font` génère des noms de classes et des variables CSS uniques pour isoler les polices. Cependant, plusieurs fichiers SCSS référençaient les noms de polices en dur ("Poppins", "Inter") au lieu d'utiliser les variables CSS injectées par `next/font` (`var(--font-poppins)`, `var(--font-inter)`). En conséquence : 1. Le navigateur téléchargeait la police préchargée par `next/font`. 2. Le CSS demandait une autre police (système ou téléchargée séparément), rendant le préchargement inutile et causant un double téléchargement. 3. Risque de Cumulative Layout Shift (CLS) accru. #### Correctifs 1. **Variables globales (`variables.scss`) :** - Remplacement de `font-family: "Poppins", ...` par `font-family: var(--font-poppins), ...`. 2. **Styles globaux (`index.scss`) :** - Remplacement de `font-family: "Inter", ...` par `font-family: var(--font-inter), ...`. 3. **Composants spécifiques :** - Remplacement des fallbacks `Poppins` par `sans-serif` dans les fichiers suivants pour utiliser correctement la variable CSS héritée ou définie : - `src/front/Components/Elements/NumberPicker/classes.module.scss` - `src/front/Components/DesignSystem/SearchBar/classes.module.scss` - `src/front/Components/DesignSystem/Form/TextField/classes.module.scss` - `src/front/Components/DesignSystem/Form/TextareaField/classes.module.scss` - `src/front/Components/DesignSystem/AutocompleteMultiSelect/ChipContainer/classes.module.scss` - `src/front/Components/DesignSystem/Footer/classes.module.scss` #### Impacts - Disparition de l'avertissement dans la console. - Meilleure performance de chargement des polices (pas de double téléchargement). - Élimination du Layout Shift (CLS) lié aux polices. - Cohérence totale de la gestion des polices via `next/font`. #### Modalités de déploiement Déploiement standard de l'application frontend `lecoffreio`. #### Modalités d'analyse Vérifier dans la console du navigateur (Chrome/Firefox) que l'avertissement concernant le préchargement des polices n'apparaît plus lors du chargement de l'application. Vérifier visuellement que les polices s'affichent correctement (Inter pour le corps, Poppins pour les titres/textes spécifiques). ### Correction de l'affichage des polices et support des Portals #### Problème Les polices (Inter et Poppins) chargées via `next/font` ne s'affichaient pas correctement : 1. Avertissement "Ressource préchargée non utilisée". 2. Polices non appliquées sur les éléments rendus via React Portals (modales, tooltips, etc.). 3. Polices non appliquées sur le `body` (fallback vers Serif). #### Cause 1. `next/font` génère des noms de classes et de variables CSS uniques (hashés). 2. Pour que Next.js inclue les définitions `@font-face` dans le build, les classes générées (`inter.variable`, etc.) doivent être utilisées dans le rendu. 3. Les Portals rendent leur contenu en dehors de l'arbre React principal (souvent directement dans `body`), donc ils n'héritent pas des variables CSS définies sur un conteneur racine (`#__next > div`). 4. Le fichier `index.scss` ne définissait pas explicitement `font-family` sur le `body`, laissant le navigateur utiliser la police par défaut (Serif) si aucune autre règle ne s'appliquait. 5. `variables.scss` utilisait `Poppins` comme police de texte par défaut au lieu de `Inter`. #### Solution ##### 1. Chargement des polices (`src/pages/_app.tsx`) - Configuration de `Inter` et `Poppins` avec `next/font/google` et l'option `variable`. - Application des classes générées (`inter.variable`, `poppins.variable`) sur un `div` conteneur entourant l'application. Cela force Next.js à inclure les polices dans le build. - Injection des variables CSS au niveau global (`:root`) via ` {/* ... */} ); return (
{/* ... providers ... */} {getLayout(page)}
); ``` ##### 2. Application des styles (`src/front/index.scss`) - Ajout explicite de la famille de police sur le `body` pour assurer l'héritage par défaut. ```scss // src/front/index.scss body { // ... font-family: var(--font-inter), sans-serif; } ``` ##### 3. Variables SCSS (`src/front/Themes/variables.scss`) - Mise à jour de `--font-text-family` pour utiliser `Inter` (via `var(--font-inter)`) au lieu de `Poppins`. ```scss // src/front/Themes/variables.scss --font-text-family: var(--font-inter), sans-serif; ``` #### Vérification - Vérifier que les requêtes réseau pour les fichiers `.woff2` sont bien effectuées (Code 200). - Vérifier que les variables CSS `--font-inter` et `--font-poppins` sont définies sur `:root` et sur le conteneur principal. - Vérifier que le texte du corps utilise bien `Inter` et les titres `Poppins`. ### Suppression des logs info et debug **Date :** 04/03/2026 **Auteur :** Équipe 4NK #### Problème Les logs de niveau `info` et `debug` étaient trop verbeux et pouvaient potentiellement exposer des informations sensibles ou encombrer les sorties de logs en production. #### Impacts - Réduction du volume de logs. - Amélioration de la sécurité (moins de risque de fuite d'informations). - Meilleure lisibilité des logs (focus sur `warn` et `error`). #### Cause Configuration par défaut des wrappers de logs qui laissait passer tous les niveaux. #### Root Cause Absence de filtrage ou de désactivation explicite des niveaux `info` et `debug` dans les utilitaires de logging centralisés. #### Corrections - Désactivation des méthodes `log`, `info` et `debug` dans `lecoffre-back-main/src/common/helpers/LogSanitizer.ts`. - Désactivation des méthodes `info` et `debug` dans `lecoffre-front-main/src/front/Services/LoggerService/LoggerServiceClass.ts`. - Correction d'une erreur de type (variable inutilisée) dans `lecoffre-front-main/src/front/Api/BaseApiServiceFactory.ts` détectée lors du typecheck. #### Modifications - `lecoffre-back-main/src/common/helpers/LogSanitizer.ts` : Commentaire du corps des fonctions `log`, `info`, `debug`. - `lecoffre-front-main/src/front/Services/LoggerService/LoggerServiceClass.ts` : Commentaire du corps des fonctions `info`, `debug`. - `lecoffre-front-main/src/front/Api/BaseApiServiceFactory.ts` : Suppression de la variable `token` inutilisée. #### Modalités de déploiement Déploiement standard via le script de déploiement. Aucune action spécifique requise. #### Modalités d'analyse Vérifier que les logs `info` et `debug` n'apparaissent plus dans les sorties de l'application, mais que les `warn` et `error` sont toujours présents. ### Fix: Erreur "this.officesRibService.uploadEncryptedRibFromFiles is not a function" #### Problème Lors de l'upload d'un RIB par un notaire, une erreur 500 survenait avec le message `technicalMessage: 'this.officesRibService.uploadEncryptedRibFromFiles is not a function'`. #### Cause Racine Le service `OfficesRibService` était enregistré deux fois dans le conteneur d'injection de dépendances (TypeDI) : 1. Via le décorateur `@Service()` sur la classe. 2. Via `Container.set()` avec une factory à la fin du fichier. Cette double déclaration créait une ambiguïté. TypeDI injectait probablement une instance créée via le constructeur par défaut (à cause de `@Service()`) au lieu d'utiliser la factory. Comme le constructeur attend une interface `OfficesRibServiceDeps` que TypeDI ne peut pas résoudre automatiquement, l'instance injectée était probablement mal initialisée ou vide. #### Correctifs 1. Suppression du décorateur `@Service()` dans `OfficesRibService.ts` pour forcer l'utilisation de la factory définie par `Container.set()`. 2. Suppression du décorateur `@Service()` dans `OfficesAnchorService.ts` (même pattern identifié). 3. Ajout de vérifications de sécurité dans le constructeur de `OfficesService.ts` pour lever une erreur explicite si les services injectés sont invalides ou incomplets. #### Impacts - `lecoffre-back-main/src/services/notary/OfficesService/OfficesRibService.ts` - `lecoffre-back-main/src/services/notary/OfficesService/OfficesAnchorService.ts` - `lecoffre-back-main/src/services/notary/OfficesService/OfficesService.ts` #### Modalités de déploiement Déploiement standard du backend. #### Modalités d'analyse Vérifier les logs lors de l'upload de RIB. L'erreur ne doit plus apparaître. Si une erreur d'injection survient, elle sera désormais explicite grâce aux checks ajoutés dans `OfficesService`. ### Correction de l'erreur "Response body is already used" **Date :** 04/03/2026 **Auteur :** Équipe 4NK #### Problème L'application frontend rencontrait une erreur critique lors du chargement des textes (`SiteTextsProvider`) ou d'autres appels API : `Failed to execute 'clone' on 'Response': Response body is already used` Cette erreur provoquait potentiellement un crash ou un comportement instable, empêchant notamment le chargement correct des polices (si lié au rendu bloqué). #### Cause Dans `BaseApiServiceResponseHandler.handleErrorResponse` (et potentiellement `processResponse`), la méthode `response.clone()` était appelée sans vérifier si le corps de la réponse avait déjà été consommé (`response.bodyUsed`). Si une requête échouait (ex: 404, 500) et que le corps avait été lu précédemment (par exemple par `response.text()` dans `processResponse` avant de détecter une erreur JSON, ou par une autre logique), l'appel à `clone()` levait une exception non gérée correctement à ce niveau. #### Correctifs 1. **Sécurisation de `handleErrorResponse` (`BaseApiServiceResponseHandler.ts`) :** - Ajout d'une vérification `if (!response.bodyUsed)` avant de tenter `response.clone()`. - Gestion du cas où le corps est déjà utilisé : on ne tente pas de le relire, on renvoie un message d'erreur générique ("Response body was already consumed") ou on utilise le texte extrait précédemment si disponible (via `responseCopy`). 2. **Sécurisation de `processResponse` (`responseHelpers.ts`) :** - Ajout d'une vérification similaire avant `response.clone()` pour le parsing JSON. - Passage explicite de `responseClone` (nullable) à `parseJsonResponse`. #### Impacts - Disparition de l'erreur "Response body is already used". - Meilleure résilience de l'application face aux erreurs API. - Les erreurs API réelles (404, 500) seront désormais correctement loggées et traitées au lieu d'être masquées par un crash technique. #### Modalités de déploiement Déploiement standard de l'application frontend `lecoffreio`. #### Modalités d'analyse - Vérifier dans la console que l'erreur "Response body is already used" n'apparaît plus. - Si des erreurs API surviennent (ex: `SiteTextsApi`), vérifier qu'elles sont loggées avec leur statut et message corrects. ### Correction du chargement des textes du site et de l'erreur response.clone() **Date :** 04/03/2026 **Auteur :** Équipe 4NK #### Problème L'application frontend rencontrait une erreur critique au démarrage : `Failed to execute 'clone' on 'Response': Response body is already used`. Cette erreur empêchait le chargement des textes du site (`SiteTexts`), ce qui pouvait entraîner un affichage dégradé (polices non chargées, textes manquants). #### Impacts - Échec du chargement des `SiteTexts`. - Affichage potentiellement cassé de l'application. - Logs d'erreurs critiques dans la console. #### Cause Dans `responseHelpers.ts`, la méthode `response.text()` était appelée pour lire le corps de la réponse, consommant ainsi le flux. Ensuite, `response.clone()` était appelé pour passer une copie à `parseJsonResponse`, ce qui échouait car le corps était déjà consommé. #### Root Cause Mauvaise séquence d'opérations : tentative de clonage d'une réponse après consommation de son corps. #### Corrections - Modification de `lecoffre-front-main/src/front/Api/helpers/responseHelpers.ts` pour cloner la réponse (`response.clone()`) **avant** de consommer le corps avec `response.text()`. #### Modifications - `lecoffre-front-main/src/front/Api/helpers/responseHelpers.ts` : Déplacement de l'appel à `response.clone()` avant le bloc `try/catch` de lecture du texte. #### Modalités de déploiement Déploiement standard via le script de déploiement. #### Modalités d'analyse Vérifier dans les logs du navigateur que l'erreur `Failed to execute 'clone' on 'Response'` a disparu et que les appels à `/api/v1/public/site-texts` réussissent. Vérifier que l'application s'affiche correctement avec les bonnes polices. ### Stockage des e-mails clients (V1 et V2) et lecture seule base V1 **Où sont stockés les e-mails des clients :** - Table **`contacts`** : colonne **`email`** (type `character varying(255)`, NOT NULL). Les clients (customers) sont liés via `customers.contact_uid` → `contacts.uid`. L’email client est donc **`contacts.email`**. - Les utilisateurs (users) ont aussi un lien `users.contact_uid` → `contacts.uid` et peuvent avoir un champ email propre sur `users`. **Vérification en lecture seule sur la base V1 (ex. depuis prod) :** - Ne pas se connecter en SSH depuis l’automatisation (règle projet). Exécuter le script **sur** la cible (après connexion manuelle ou via un job que vous lancez). - Sur le serveur (ex. prod) : depuis `$APP_ROOT` (ex. `/srv/4NK/prod.lecoffreio.4nkweb.com`), lancer : - `bash deploy/scripts_v2/remote/read-v1-contacts-emails.sh prod "$APP_ROOT"` - Le script lit la config V1 dans `.secrets//.env..connectDB` (DATABASE_V1_*), se connecte en **lecture seule** à la base V1 et affiche : structure de la table `contacts`, effectifs contacts/customers, statistiques sur les longueurs d’email (aucun email en clair). - Prérequis sur la cible : `psql`, et le fichier `.env..connectDB` présent dans `.secrets//`. ## Lien avec Gitea et traitement des issues par un agent Le dépôt est hébergé sur Gitea à l’URL **https://git.4nkweb.com/4nk/lecoffre_ng** (VM distante, distincte du serveur « services » 192.168.1.104). Les issues sont visibles sur **https://git.4nkweb.com/4nk/lecoffre_ng/issues**. ### Comment le projet « branche » sur les issues - **Convention actuelle** : le README décrit une branche feature (`feature/nouvelle-fonctionnalite`), push direct (pas de pull request). Il n’existe pas de convention formelle « une branche par issue » ou « nom de branche = numéro d’issue » dans la doc. - **Lien possible** : pour associer une branche à une issue, on peut utiliser par exemple `issue/` ou `feature/issue--titre-court`. Le numéro d’issue permet de retrouver le ticket dans Gitea et, si besoin, de le commenter ou de le fermer depuis un agent. ### Arbitrage des pull requests en cours - **Workflow** : le code est intégré par **push direct** sur les branches (test, pprod, prod). Les pull requests ouvertes sur Gitea peuvent être **fermées sans merger** : le code pertinent est déjà sur les branches via push direct. Pour nettoyer : sur Gitea (https://git.4nkweb.com/4nk/lecoffre_ng/pulls), fermer chaque PR ouverte en indiquant que le workflow est désormais push direct (pas de merge via PR). ### Traitement des tickets (issues) depuis un agent L’accès **Git** (clone, push) utilise **SSH** (`git@git.4nkweb.com:4nk/lecoffre_ng.git`) ; la clé SSH de la machine donne les droits nécessaires pour pousser des branches. L’**API Gitea** (lister les issues, lire une issue, ajouter un commentaire, etc.) est en **HTTPS** et nécessite un **token d’accès** (Personal Access Token), pas la clé SSH. **Prérequis sur la machine qui lance l’agent :** 1. **Token Gitea** : créer un token dans Gitea (Settings → Applications → Generate New Token) avec les scopes nécessaires (ex. `read:issue`, `write:issue` pour commenter). Stocker le token dans une variable d’environnement (ex. `GITEA_TOKEN`) ou dans un fichier non versionné (ex. `.secrets/gitea-token`), jamais dans le dépôt. 2. **Accès réseau** : la machine doit pouvoir joindre `https://git.4nkweb.com` (le Gitea est sur une VM distante). **Lister les issues ouvertes (exemple curl) :** ```bash curl -s -H "Accept: application/json" \ -H "Authorization: token ${GITEA_TOKEN}" \ "https://git.4nkweb.com/api/v1/repos/4nk/lecoffre_ng/issues?state=open&page=1&limit=50" ``` **Récupérer une issue par numéro :** ```bash curl -s -H "Accept: application/json" \ -H "Authorization: token ${GITEA_TOKEN}" \ "https://git.4nkweb.com/api/v1/repos/4nk/lecoffre_ng/issues/" ``` **Workflow type pour un agent :** 1. Récupérer la liste des issues ouvertes (ou filtrer par label/milestone si besoin). 2. Pour chaque issue (ou une issue ciblée) : - Créer une branche dédiée (ex. `issue/123` ou `feature/issue-123-titre`) depuis la branche de travail (ex. `test`). - Lancer l’agent adapté selon le type de ticket : **/fix** pour une anomalie, **/evol** pour une évolution (voir `.cursor/agents/fix.md`, `.cursor/agents/evol.md`), en utilisant le titre et la description de l’issue comme consigne. - Après correction/évolution : commit, push (`/push-by-script`), **ne jamais créer de pull request**. Optionnellement commenter l'issue via l’API Gitea (POST `/repos/4nk/lecoffre_ng/issues//comments`). **Référence API Gitea :** (tag « issue » pour les endpoints issues). **Scripts et agent dédiés :** le dossier **`gitea-issues/`** à la racine du dépôt contient les scripts (list-open-issues.sh, get-issue.sh, print-issue-prompt.sh, create-branch-for-issue.sh, comment-issue.sh) et l'agent **/gitea-issues-process** (`.cursor/agents/gitea-issues-process.md`) orchestre le traitement des tickets en s'appuyant uniquement sur ces scripts. Voir `gitea-issues/README.md`.