NeahNew/MISSION_CREATION_WORKFLOW_DETAILED.md
2026-01-04 13:12:51 +01:00

983 lines
29 KiB
Markdown

# 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<boolean> {
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<any> {
// 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