# Workflow Détaillé : Création de Mission - Prisma, Minio et N8N ## 📋 Vue d'Ensemble Ce document trace **chaque étape** du workflow de création de mission, depuis le formulaire frontend jusqu'aux intégrations externes via N8N, en passant par Prisma (base de données) et Minio (stockage de fichiers). --- ## 🔄 Flux Complet - Vue d'Ensemble ``` ┌─────────────────────────────────────────────────────────────┐ │ 1. FRONTEND - MissionsAdminPanel │ │ - Validation des champs │ │ - Préparation des données (base64 pour fichiers) │ │ - POST /api/missions │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 2. BACKEND - POST /api/missions │ │ ├─ Authentification (NextAuth) │ │ ├─ Validation des champs requis │ │ ├─ STEP 1: Prisma.mission.create() │ │ ├─ STEP 2: Prisma.missionUser.createMany() │ │ ├─ STEP 3: Upload Logo → Minio │ │ ├─ STEP 4: Upload Attachments → Minio │ │ ├─ STEP 5: Vérification fichiers Minio │ │ └─ STEP 6: N8N Workflow Trigger │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 3. N8N WORKFLOW │ │ - Création Leantime Project │ │ - Création Outline Collection │ │ - Création RocketChat Channel │ │ - Création Gitea Repository │ │ - Création Penpot Project │ └─────────────────────────────────────────────────────────────┘ ``` --- ## 📝 ÉTAPE 1 : Frontend - Préparation des Données ### Fichier : `components/missions/missions-admin-panel.tsx` ### 1.1 Validation (`validateMission()`) **Lignes 369-397** ```typescript const validateMission = () => { const requiredFields = { name: !!missionData.name, oddScope: Array.isArray(missionData.oddScope) && missionData.oddScope.length > 0, niveau: !!missionData.niveau, intention: !!missionData.intention, missionType: !!missionData.missionType, donneurDOrdre: !!missionData.donneurDOrdre, projection: !!missionData.projection, participation: !!missionData.participation, gardiens: gardienDuTemps !== null && gardienDeLaParole !== null && gardienDeLaMemoire !== null }; // Vérifie que tous les champs requis sont remplis // Retourne false si un champ manque } ``` **Champs requis** : - ✅ `name` : Nom de la mission - ✅ `oddScope` : Array avec au moins 1 ODD - ✅ `niveau` : A/B/C/S - ✅ `intention` : Description - ✅ `missionType` : remote/onsite/hybrid - ✅ `donneurDOrdre` : individual/group/organization - ✅ `projection` : short/medium/long - ✅ `participation` : volontaire/cooptation - ✅ `gardiens` : Les 3 gardiens doivent être assignés ### 1.2 Préparation des Données (`handleSubmitMission()`) **Lignes 400-460** ```typescript const handleSubmitMission = async () => { // 1. Validation if (!validateMission()) return; // 2. Préparation des gardiens const guardians = { "gardien-temps": gardienDuTemps, "gardien-parole": gardienDeLaParole, "gardien-memoire": gardienDeLaMemoire }; // 3. Construction de l'objet de soumission const missionSubmitData = { ...missionData, // name, oddScope, niveau, intention, etc. services: selectedServices, // ["Gite", "ArtLab", "Calcul"] profils: selectedProfils, // ["DataIntelligence", "Expression", ...] guardians, // { "gardien-temps": userId, ... } volunteers: volontaires, // [userId1, userId2, ...] logo: missionData.logo // { data: "data:image/png;base64,...", name, type } }; // 4. Envoi à l'API const response = await fetch('/api/missions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(missionSubmitData) }); } ``` **Format du logo** (si présent) : ```typescript logo: { data: "data:image/png;base64,iVBORw0KGgoAAAANS...", // Base64 avec préfixe name: "logo.png", type: "image/png" } ``` **Format des attachments** (si présents) : ```typescript attachments: [ { data: "data:application/pdf;base64,JVBERi0xLjQKJe...", name: "document.pdf", type: "application/pdf" } ] ``` --- ## 🗄️ ÉTAPE 2 : Backend - POST /api/missions ### Fichier : `app/api/missions/route.ts` ### 2.1 Authentification et Validation **Lignes 205-224** ```typescript export async function POST(request: Request) { // 1. Vérification de l'authentification const { authorized, userId } = await checkAuth(request); if (!authorized || !userId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } // 2. Parsing du body const body = await request.json(); // 3. Validation minimale (name et oddScope requis) if (!body.name || !body.oddScope) { return NextResponse.json({ error: 'Missing required fields', missingFields: ['name', 'oddScope'].filter(field => !body[field]) }, { status: 400 }); } } ``` ### 2.2 STEP 1 : Création de la Mission en Base de Données **Lignes 226-248** ```typescript // Préparation des données pour Prisma const missionData = { name: body.name, oddScope: body.oddScope, // Array de strings niveau: body.niveau, intention: body.intention, missionType: body.missionType, donneurDOrdre: body.donneurDOrdre, projection: body.projection, services: body.services, // Array de strings profils: body.profils, // Array de strings participation: body.participation, creatorId: userId, // ID de l'utilisateur connecté logo: null, // Sera mis à jour après upload }; // Création en base de données const mission = await prisma.mission.create({ data: missionData }); // Résultat : mission.id est maintenant disponible // Exemple : mission.id = "abc-123-def-456" ``` **Schéma Prisma** : ```prisma model Mission { id String @id @default(uuid()) name String logo String? // null pour l'instant oddScope String[] niveau String intention String missionType String donneurDOrdre String projection String services String[] participation String? profils String[] creatorId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations creator User @relation(...) missionUsers MissionUser[] attachments Attachment[] } ``` **Points importants** : - ✅ La mission est créée **AVANT** l'upload des fichiers - ✅ Le `mission.id` est généré automatiquement (UUID) - ✅ Le champ `logo` est `null` pour l'instant - ✅ Tous les champs sont sauvegardés sauf les fichiers ### 2.3 STEP 2 : Création des MissionUsers (Gardiens + Volontaires) **Lignes 250-283** ```typescript // Préparation du tableau de MissionUsers const missionUsers = []; // 2.1 Ajout des gardiens if (body.guardians) { for (const [role, guardianId] of Object.entries(body.guardians)) { if (guardianId) { missionUsers.push({ missionId: mission.id, // ID de la mission créée userId: guardianId, // ID de l'utilisateur gardien role: role // "gardien-temps", "gardien-parole", "gardien-memoire" }); } } } // 2.2 Ajout des volontaires if (body.volunteers && body.volunteers.length > 0) { for (const volunteerId of body.volunteers) { missionUsers.push({ missionId: mission.id, userId: volunteerId, role: 'volontaire' }); } } // 2.3 Création en batch dans Prisma if (missionUsers.length > 0) { await prisma.missionUser.createMany({ data: missionUsers }); } ``` **Schéma Prisma MissionUser** : ```prisma model MissionUser { id String @id @default(uuid()) role String // 'gardien-temps', 'gardien-parole', 'gardien-memoire', 'volontaire' missionId String userId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt mission Mission @relation(...) user User @relation(...) @@unique([missionId, userId, role]) // Un utilisateur ne peut avoir qu'un rôle par mission } ``` **Exemple de données créées** : ```typescript missionUsers = [ { missionId: "abc-123", userId: "user-1", role: "gardien-temps" }, { missionId: "abc-123", userId: "user-2", role: "gardien-parole" }, { missionId: "abc-123", userId: "user-3", role: "gardien-memoire" }, { missionId: "abc-123", userId: "user-4", role: "volontaire" }, { missionId: "abc-123", userId: "user-5", role: "volontaire" } ] ``` **Points importants** : - ✅ Utilisation de `createMany()` pour performance (1 requête au lieu de N) - ✅ Contrainte unique : un utilisateur ne peut avoir qu'un rôle par mission - ✅ Les gardiens sont obligatoires, les volontaires optionnels ### 2.4 STEP 3 : Upload du Logo vers Minio **Lignes 285-310** ```typescript let logoPath = null; if (body.logo?.data) { try { // 3.1 Conversion base64 → Buffer → File const base64Data = body.logo.data.split(',')[1]; // Retire "data:image/png;base64," const buffer = Buffer.from(base64Data, 'base64'); const file = new File( [buffer], body.logo.name || 'logo.png', { type: body.logo.type || 'image/png' } ); // 3.2 Upload vers Minio const { filePath } = await uploadMissionLogo(userId, mission.id, file); logoPath = filePath; // Ex: "missions/abc-123/logo.png" uploadedFiles.push({ type: 'logo', path: filePath }); // 3.3 Mise à jour de la mission avec le chemin du logo await prisma.mission.update({ where: { id: mission.id }, data: { logo: filePath } }); } catch (uploadError) { throw new Error('Failed to upload logo'); } } ``` **Fonction `uploadMissionLogo()` - `lib/mission-uploads.ts`** : **Lignes 96-145** ```typescript export async function uploadMissionLogo( userId: string, missionId: string, file: File ): Promise<{ filePath: string }> { // 1. Génération du chemin const fileExtension = file.name.substring(file.name.lastIndexOf('.')); const filePath = getMissionLogoPath(userId, missionId, fileExtension); // Résultat : "missions/{missionId}/logo.png" // 2. Conversion pour Minio (retire le préfixe "missions/") const minioPath = filePath.replace(/^missions\//, ''); // Résultat : "{missionId}/logo.png" // 3. Conversion File → Buffer const arrayBuffer = await file.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); // 4. Upload vers Minio via S3 SDK await s3Client.send(new PutObjectCommand({ Bucket: 'missions', // Bucket Minio Key: minioPath, // "{missionId}/logo.png" Body: buffer, // Contenu du fichier ContentType: file.type, // "image/png" ACL: 'public-read' // Accès public en lecture })); return { filePath }; // Retourne le chemin complet avec préfixe } ``` **Configuration Minio** : ```typescript // lib/mission-uploads.ts const s3Client = new S3Client({ region: 'us-east-1', endpoint: 'https://dome-api.slm-lab.net', // Endpoint Minio credentials: { accessKeyId: '4aBT4CMb7JIMMyUtp4Pl', secretAccessKey: 'HGn39XhCIlqOjmDVzRK9MED2Fci2rYvDDgbLFElg' }, forcePathStyle: true // Requis pour MinIO }); ``` **Structure dans Minio** : ``` Bucket: missions └── {missionId}/ └── logo.png ``` **Points importants** : - ✅ Le logo est uploadé **APRÈS** la création de la mission (pour avoir le missionId) - ✅ Le chemin est mis à jour dans Prisma après l'upload - ✅ Le fichier est stocké avec ACL `public-read` pour accès public - ✅ Le chemin stocké inclut le préfixe `missions/` pour cohérence ### 2.5 STEP 4 : Upload des Attachments vers Minio **Lignes 312-343** ```typescript if (body.attachments && body.attachments.length > 0) { try { // 4.1 Traitement parallèle de tous les attachments const attachmentPromises = body.attachments.map(async (attachment: any) => { // Conversion base64 → Buffer → File const base64Data = attachment.data.split(',')[1]; const buffer = Buffer.from(base64Data, 'base64'); const file = new File( [buffer], attachment.name || 'attachment', { type: attachment.type || 'application/octet-stream' } ); // 4.2 Upload vers Minio const { filePath, filename, fileType, fileSize } = await uploadMissionAttachment(userId, mission.id, file); uploadedFiles.push({ type: 'attachment', path: filePath }); // 4.3 Création de l'enregistrement Attachment en base return prisma.attachment.create({ data: { missionId: mission.id, filename, // "document.pdf" filePath, // "missions/abc-123/attachments/document.pdf" fileType, // "application/pdf" fileSize, // 123456 (bytes) uploaderId: userId } }); }); // 4.4 Attente de tous les uploads (parallèle) await Promise.all(attachmentPromises); } catch (attachmentError) { throw new Error('Failed to upload attachments'); } } ``` **Fonction `uploadMissionAttachment()` - `lib/mission-uploads.ts`** : **Lignes 148-210** ```typescript export async function uploadMissionAttachment( userId: string, missionId: string, file: File ): Promise<{ filename: string; filePath: string; fileType: string; fileSize: number; }> { // 1. Génération du chemin const filePath = getMissionAttachmentPath(userId, missionId, file.name); // Résultat : "missions/{missionId}/attachments/{filename}" // 2. Conversion pour Minio const minioPath = filePath.replace(/^missions\//, ''); // Résultat : "{missionId}/attachments/{filename}" // 3. Conversion File → Buffer const arrayBuffer = await file.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); // 4. Upload vers Minio await s3Client.send(new PutObjectCommand({ Bucket: 'missions', Key: minioPath, Body: buffer, ContentType: file.type, ACL: 'public-read' })); return { filename: file.name, filePath, // Chemin complet avec préfixe fileType: file.type, fileSize: file.size }; } ``` **Schéma Prisma Attachment** : ```prisma model Attachment { id String @id @default(uuid()) filename String // "document.pdf" filePath String // "missions/{missionId}/attachments/{filename}" fileType String // "application/pdf" fileSize Int // 123456 missionId String uploaderId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt mission Mission @relation(...) uploader User @relation(...) } ``` **Structure dans Minio** : ``` Bucket: missions └── {missionId}/ ├── logo.png └── attachments/ ├── document1.pdf ├── document2.docx └── image.jpg ``` **Points importants** : - ✅ Uploads en **parallèle** avec `Promise.all()` pour performance - ✅ Chaque attachment crée un enregistrement Prisma séparé - ✅ Le `uploaderId` est l'utilisateur qui a créé la mission - ✅ Les fichiers sont stockés avec ACL `public-read` ### 2.6 STEP 5 : Vérification des Fichiers dans Minio **Lignes 345-365** ```typescript // 5.1 Vérification du logo if (logoPath) { const logoExists = await verifyFileExists(logoPath); if (!logoExists) { throw new Error('Logo file not found in Minio'); } } // 5.2 Vérification des attachments if (body.attachments?.length > 0) { const attachmentVerifications = uploadedFiles .filter(f => f.type === 'attachment') .map(f => verifyFileExists(f.path)); const attachmentResults = await Promise.all(attachmentVerifications); if (attachmentResults.some(exists => !exists)) { throw new Error('One or more attachment files not found in Minio'); } } ``` **Fonction `verifyFileExists()` - Lignes 191-202** ```typescript async function verifyFileExists(filePath: string): Promise { try { await s3Client.send(new HeadObjectCommand({ Bucket: 'missions', Key: filePath.replace('missions/', '') // Retire le préfixe })); return true; // Fichier existe } catch (error) { return false; // Fichier n'existe pas } } ``` **Points importants** : - ✅ Vérification **AVANT** de déclencher N8N - ✅ Utilise `HeadObjectCommand` (légère, ne télécharge pas le fichier) - ✅ Si un fichier manque, le workflow s'arrête avec erreur ### 2.7 STEP 6 : Déclenchement du Workflow N8N **Lignes 367-393** ```typescript // 6.1 Préparation des données pour N8N const n8nData = { ...body, // Toutes les données de la mission creatorId: userId, // ID du créateur logoPath: logoPath, // Chemin du logo (ou null) config: { N8N_API_KEY: process.env.N8N_API_KEY, MISSION_API_URL: process.env.NEXT_PUBLIC_API_URL } }; // 6.2 Déclenchement du workflow const n8nService = new N8nService(); const workflowResult = await n8nService.triggerMissionCreation(n8nData); // 6.3 Vérification du résultat if (!workflowResult.success) { throw new Error(workflowResult.error || 'N8N workflow failed'); } // 6.4 Retour succès return NextResponse.json({ success: true, mission, message: 'Mission created successfully with all integrations' }); ``` --- ## 🔗 ÉTAPE 3 : Service N8N ### Fichier : `lib/services/n8n-service.ts` ### 3.1 Configuration **Lignes 3-17** ```typescript export class N8nService { private webhookUrl: string; private rollbackWebhookUrl: string; private apiKey: string; constructor() { this.webhookUrl = process.env.N8N_WEBHOOK_URL || 'https://brain.slm-lab.net/webhook/mission-created'; this.rollbackWebhookUrl = process.env.N8N_ROLLBACK_WEBHOOK_URL || 'https://brain.slm-lab.net/webhook/mission-rollback'; this.apiKey = process.env.N8N_API_KEY || ''; } } ``` ### 3.2 Nettoyage et Validation des Données **Lignes 19-49** ```typescript async triggerMissionCreation(data: any): Promise { // Nettoyage des données const cleanData = { name: data.name, oddScope: Array.isArray(data.oddScope) ? data.oddScope : [data.oddScope], niveau: data.niveau || 'default', intention: data.intention?.trim() || '', missionType: data.missionType || 'default', donneurDOrdre: data.donneurDOrdre || 'default', projection: data.projection || 'default', services: Array.isArray(data.services) ? data.services : [], participation: data.participation || 'default', profils: Array.isArray(data.profils) ? data.profils : [], guardians: data.guardians || {}, volunteers: Array.isArray(data.volunteers) ? data.volunteers : [], creatorId: data.creatorId, config: { ...data.config, N8N_API_KEY: this.apiKey, MISSION_API_URL: process.env.NEXT_PUBLIC_API_URL || 'https://api.slm-lab.net/api' } }; } ``` **Points importants** : - ✅ Normalisation des arrays (assure qu'ils sont bien des arrays) - ✅ Valeurs par défaut pour éviter les undefined - ✅ Trim de l'intention pour retirer les espaces - ✅ Conservation de la config avec les clés API ### 3.3 Envoi au Webhook N8N **Lignes 73-96** ```typescript const response = await fetch(this.webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': this.apiKey // Authentification }, body: JSON.stringify(cleanData) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP error! status: ${response.status}, body: ${errorText}`); } ``` ### 3.4 Traitement de la Réponse **Lignes 98-143** ```typescript const responseText = await response.text(); try { const result = JSON.parse(responseText); // Détection d'erreurs partielles if (result.error || result.message?.includes('failed')) { const errorMessage = result.message || result.error; const failedServices = { gitRepo: errorMessage.includes('Git repository creation failed'), leantimeProject: errorMessage.includes('Leantime project creation failed'), docCollection: errorMessage.includes('Documentation collection creation failed'), rocketChatChannel: errorMessage.includes('RocketChat channel creation failed') }; // Retourne succès avec erreurs partielles return { success: true, results: { ...result, failedServices } }; } return { success: true, results: result }; } catch (parseError) { // Si la réponse n'est pas JSON, considère comme succès return { success: true, results: { logoUrl: null, leantimeProjectId: null, outlineCollectionId: null, rocketChatChannelId: null, giteaRepositoryUrl: null } }; } ``` **Format de réponse attendu de N8N** : ```json { "leantimeProjectId": "project-123", "outlineCollectionId": "collection-456", "rocketChatChannelId": "channel-789", "giteaRepositoryUrl": "https://git.slm-lab.net/mission-abc", "penpotProjectId": "penpot-xyz" } ``` **Intégrations créées par N8N** : 1. **Leantime** : Projet de gestion de projet 2. **Outline** : Collection de documentation 3. **RocketChat** : Canal de communication 4. **Gitea** : Repository Git 5. **Penpot** : Projet de design **Points importants** : - ✅ Gestion des erreurs partielles (certains services peuvent échouer) - ✅ Si la réponse n'est pas JSON, considère comme succès (workflow déclenché) - ✅ Les IDs retournés ne sont **PAS** sauvegardés en base (TODO) --- ## 🧹 Gestion des Erreurs et Cleanup ### Fichier : `app/api/missions/route.ts` - Lignes 398-418 ```typescript catch (error) { console.error('Error in mission creation:', error); // Cleanup: Suppression de tous les fichiers uploadés for (const file of uploadedFiles) { try { await s3Client.send(new DeleteObjectCommand({ Bucket: 'missions', Key: file.path.replace('missions/', '') })); console.log('Cleaned up file:', file.path); } catch (cleanupError) { console.error('Error cleaning up file:', file.path, cleanupError); } } return NextResponse.json({ error: 'Failed to create mission', details: error instanceof Error ? error.message : String(error) }, { status: 500 }); } ``` **Scénarios de cleanup** : 1. ✅ Erreur lors de l'upload du logo → Suppression du logo 2. ✅ Erreur lors de l'upload d'un attachment → Suppression de tous les fichiers 3. ✅ Erreur lors de la vérification Minio → Suppression de tous les fichiers 4. ✅ Erreur N8N → Suppression de tous les fichiers **Points importants** : - ✅ La mission reste en base même en cas d'erreur (orphan) - ✅ Les MissionUsers restent en base même en cas d'erreur - ✅ Seuls les fichiers Minio sont nettoyés - ⚠️ **TODO** : Rollback complet (suppression mission + users si erreur) --- ## 📊 Résumé des Opérations Prisma ### Requêtes Prisma dans l'ordre d'exécution : 1. **`prisma.mission.create()`** - Crée la mission avec tous les champs - Génère un UUID pour `id` - `logo` = `null` initialement 2. **`prisma.missionUser.createMany()`** - Crée tous les gardiens et volontaires en une requête - Utilise `createMany()` pour performance 3. **`prisma.mission.update()`** (si logo) - Met à jour le champ `logo` avec le chemin Minio 4. **`prisma.attachment.create()`** (pour chaque attachment) - Créé en parallèle avec `Promise.all()` - Un enregistrement par fichier **Total** : 1 + 1 + (0 ou 1) + N requêtes Prisma - Minimum : 2 requêtes (mission + users) - Avec logo : 3 requêtes - Avec N attachments : 3 + N requêtes --- ## 📦 Résumé des Opérations Minio ### Uploads Minio dans l'ordre d'exécution : 1. **Logo** (si présent) - Bucket : `missions` - Key : `{missionId}/logo.png` - Path stocké : `missions/{missionId}/logo.png` 2. **Attachments** (si présents, en parallèle) - Bucket : `missions` - Key : `{missionId}/attachments/{filename}` - Path stocké : `missions/{missionId}/attachments/{filename}` 3. **Vérifications** (après uploads) - `HeadObjectCommand` pour chaque fichier - Vérifie l'existence avant N8N **Total** : 1 + N uploads + (1 + N) vérifications --- ## 🔄 Résumé du Workflow N8N ### Données envoyées à N8N : ```typescript { name: "Mission Example", oddScope: ["odd-3"], niveau: "a", intention: "Description...", missionType: "remote", donneurDOrdre: "individual", projection: "short", services: ["Gite", "ArtLab"], participation: "volontaire", profils: ["DataIntelligence", "Expression"], guardians: { "gardien-temps": "user-1", "gardien-parole": "user-2", "gardien-memoire": "user-3" }, volunteers: ["user-4", "user-5"], creatorId: "user-creator", logoPath: "missions/abc-123/logo.png", config: { N8N_API_KEY: "...", MISSION_API_URL: "https://api.slm-lab.net/api" } } ``` ### Actions N8N (workflow interne) : 1. Création projet Leantime 2. Création collection Outline 3. Création canal RocketChat 4. Création repository Gitea 5. Création projet Penpot **Note** : Les IDs retournés ne sont **PAS** sauvegardés en base actuellement. --- ## ⚠️ Points d'Attention et TODOs ### 1. Sauvegarde des IDs N8N **Problème** : Les IDs retournés par N8N (leantimeProjectId, etc.) ne sont pas sauvegardés en base. **Solution proposée** : ```typescript // Après le workflow N8N if (workflowResult.success && workflowResult.results) { await prisma.mission.update({ where: { id: mission.id }, data: { leantimeProjectId: workflowResult.results.leantimeProjectId, outlineCollectionId: workflowResult.results.outlineCollectionId, rocketChatChannelId: workflowResult.results.rocketChatChannelId, giteaRepositoryUrl: workflowResult.results.giteaRepositoryUrl, penpotProjectId: workflowResult.results.penpotProjectId } }); } ``` ### 2. Rollback Complet **Problème** : En cas d'erreur, la mission reste en base (orphan). **Solution proposée** : ```typescript catch (error) { // Suppression de la mission et des relations await prisma.mission.delete({ where: { id: mission.id } }); // Les MissionUsers et Attachments seront supprimés en cascade // Nettoyage Minio... } ``` ### 3. Credentials Minio Hardcodés **Problème** : Les credentials Minio sont hardcodés dans `lib/mission-uploads.ts`. **Solution** : Déplacer vers variables d'environnement. ### 4. Gestion des Erreurs N8N Partielles **Problème** : Si certains services N8N échouent, on continue quand même. **Solution** : Décider si on continue ou on rollback selon la criticité. --- ## 📈 Performance et Optimisations ### Optimisations actuelles : - ✅ `createMany()` pour MissionUsers (1 requête au lieu de N) - ✅ `Promise.all()` pour les attachments (parallèle) - ✅ `HeadObjectCommand` pour vérification (léger) ### Optimisations possibles : - 🔄 Transaction Prisma pour atomicité - 🔄 Batch upload Minio (si supporté) - 🔄 Retry logic pour N8N - 🔄 Cache des vérifications Minio --- ## 🔍 Debugging et Logs ### Points de logging importants : 1. **Début du workflow** : `=== Mission Creation Started ===` 2. **Création Prisma** : `Mission created successfully` 3. **Upload Minio** : `Logo upload successful` / `Attachment upload successful` 4. **Vérification** : `verifyFileExists()` logs 5. **N8N** : `=== Starting N8N Workflow ===` / `N8N Workflow Result` 6. **Erreurs** : Tous les catch blocks loggent les erreurs ### Commandes de debug : ```bash # Voir les logs du serveur npm run dev # Logs dans la console # Vérifier Minio # Accéder à https://dome-api.slm-lab.net # Bucket: missions # Vérifier Prisma npx prisma studio # Interface graphique ``` --- **Document généré le** : $(date) **Version** : 1.0 **Auteur** : Analyse complète du codebase