diff --git a/app/api/missions/[missionId]/route.ts b/app/api/missions/[missionId]/route.ts index 9b274ec..55d0fe5 100644 --- a/app/api/missions/[missionId]/route.ts +++ b/app/api/missions/[missionId]/route.ts @@ -201,7 +201,15 @@ export async function DELETE( } } - // Step 3: Call N8N deletion webhook (non-blocking) + // Step 3: Call N8N deletion webhook + logger.debug('Preparing N8N deletion webhook call', { + missionId: mission.id, + hasGiteaUrl: !!mission.giteaRepositoryUrl, + hasLeantimeId: !!mission.leantimeProjectId, + hasOutlineId: !!mission.outlineCollectionId, + hasRocketChatId: !!mission.rocketChatChannelId + }); + const n8nService = new N8nService(); const n8nData = { missionId: mission.id, @@ -216,13 +224,34 @@ export async function DELETE( penpotProjectId: mission.penpotProjectId }; - // Call N8N but don't fail if it errors - n8nService.triggerMissionDeletion(n8nData).catch(n8nError => { - logger.error('N8N deletion webhook failed (mission still deleted from database)', { - error: n8nError instanceof Error ? n8nError.message : String(n8nError), + logger.debug('Calling N8N deletion webhook', { + webhookUrl: process.env.N8N_DELETE_WEBHOOK_URL || 'https://brain.slm-lab.net/webhook/mission-delete', + hasApiKey: !!process.env.N8N_API_KEY, + dataKeys: Object.keys(n8nData) + }); + + // Call N8N but don't fail if it errors (mission deletion should still succeed) + try { + const n8nResult = await n8nService.triggerMissionDeletion(n8nData); + logger.debug('N8N deletion webhook result', { + success: n8nResult.success, + hasError: !!n8nResult.error, missionId }); - }); + + if (!n8nResult.success) { + logger.warn('N8N deletion webhook returned error (mission still deleted)', { + error: n8nResult.error, + missionId + }); + } + } catch (n8nError) { + logger.error('N8N deletion webhook failed (mission still deleted from database)', { + error: n8nError instanceof Error ? n8nError.message : String(n8nError), + errorType: n8nError instanceof Error ? n8nError.name : typeof n8nError, + missionId + }); + } // Step 4: Delete from database (cascade will handle related records) await prisma.mission.delete({ @@ -236,13 +265,23 @@ export async function DELETE( message: 'Mission deleted successfully' }); } catch (error: any) { - const { missionId: errorMissionId } = await params; + // Get missionId from params safely + let errorMissionId = 'unknown'; + try { + const paramsResult = await params; + errorMissionId = paramsResult.missionId; + } catch (paramsError) { + // If we can't get params, use unknown + logger.warn('Could not get missionId from params in error handler'); + } + logger.error('Error deleting mission', { error: error instanceof Error ? error.message : String(error), + errorStack: error instanceof Error ? error.stack : undefined, missionId: errorMissionId }); return NextResponse.json( - { error: 'Failed to delete mission', details: error.message }, + { error: 'Failed to delete mission', details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } diff --git a/app/api/missions/route.ts b/app/api/missions/route.ts index f56b80f..54734bd 100644 --- a/app/api/missions/route.ts +++ b/app/api/missions/route.ts @@ -496,16 +496,23 @@ export async function POST(request: Request) { MISSION_API_URL: process.env.NEXT_PUBLIC_API_URL } }; + + // Log detailed information for debugging logger.debug('Sending to N8N', { missionId: n8nData.missionId, name: n8nData.name, - hasLogo: !!n8nData.logoPath + hasLogo: !!n8nData.logoPath, + webhookUrl: process.env.N8N_WEBHOOK_URL || 'https://brain.slm-lab.net/webhook/mission-created', + hasApiKey: !!process.env.N8N_API_KEY, + apiKeyLength: process.env.N8N_API_KEY ? process.env.N8N_API_KEY.length : 0, + missionApiUrl: process.env.NEXT_PUBLIC_API_URL }); const workflowResult = await n8nService.triggerMissionCreation(n8nData); logger.debug('N8N workflow result', { success: workflowResult.success, - hasError: !!workflowResult.error + hasError: !!workflowResult.error, + error: workflowResult.error }); if (!workflowResult.success) { diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 85f8d70..1770164 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -161,6 +161,7 @@ services: # N8N Integration (required for mission creation workflow) N8N_API_KEY: ${N8N_API_KEY} N8N_WEBHOOK_URL: ${N8N_WEBHOOK_URL:-https://brain.slm-lab.net/webhook/mission-created} + N8N_DELETE_WEBHOOK_URL: ${N8N_DELETE_WEBHOOK_URL:-https://brain.slm-lab.net/webhook/mission-delete} # Autres variables d'environnement (ajoutez les vôtres) # volumes: diff --git a/env.production.example b/env.production.example index 059e356..8835a17 100644 --- a/env.production.example +++ b/env.production.example @@ -63,6 +63,7 @@ NEXT_PUBLIC_APP_URL=https://hub.slm-lab.net # N8N est utilisé pour créer automatiquement les projets Leantime, repos Git, etc. N8N_API_KEY=VOTRE_N8N_API_KEY N8N_WEBHOOK_URL=https://brain.slm-lab.net/webhook/mission-created +N8N_DELETE_WEBHOOK_URL=https://brain.slm-lab.net/webhook/mission-delete # ============================================ # Autres services (ajoutez selon vos besoins) diff --git a/lib/services/n8n-service.ts b/lib/services/n8n-service.ts index 0cafe2a..bff3bbf 100644 --- a/lib/services/n8n-service.ts +++ b/lib/services/n8n-service.ts @@ -13,6 +13,12 @@ export class N8nService { this.rollbackWebhookUrl = process.env.N8N_ROLLBACK_WEBHOOK_URL || 'https://brain.slm-lab.net/webhook/mission-rollback'; this.apiKey = process.env.N8N_API_KEY || ''; + logger.debug('N8nService initialized', { + webhookUrl: this.webhookUrl, + hasApiKey: !!this.apiKey, + apiKeyLength: this.apiKey ? this.apiKey.length : 0 + }); + if (!this.apiKey) { logger.error('N8N_API_KEY is not set in environment variables'); } @@ -25,22 +31,45 @@ export class N8nService { logger.debug('Triggering n8n mission deletion workflow', { missionId: data.missionId, name: data.name, - hasRepoName: !!data.repoName + hasRepoName: !!data.repoName, + repoName: data.repoName }); logger.debug('Using deletion webhook URL', { url: deleteWebhookUrl }); - logger.debug('API key present', { present: !!this.apiKey }); - - const response = await fetchWithTimeout(deleteWebhookUrl, { - method: 'POST', - timeout: 30000, // 30 seconds - headers: { - 'Content-Type': 'application/json', - 'x-api-key': this.apiKey - }, - body: JSON.stringify(data), + logger.debug('API key present', { + present: !!this.apiKey, + length: this.apiKey ? this.apiKey.length : 0 + }); + logger.debug('Request payload', { + keys: Object.keys(data), + payloadSize: JSON.stringify(data).length }); - logger.debug('Deletion webhook response', { status: response.status }); + let response: Response; + try { + response = await fetchWithTimeout(deleteWebhookUrl, { + method: 'POST', + timeout: 30000, // 30 seconds + headers: { + 'Content-Type': 'application/json', + 'x-api-key': this.apiKey + }, + body: JSON.stringify(data), + }); + + logger.debug('Deletion webhook response received', { + status: response.status, + statusText: response.statusText, + headers: Object.fromEntries(response.headers.entries()) + }); + } catch (fetchError) { + logger.error('Failed to send deletion webhook request', { + error: fetchError instanceof Error ? fetchError.message : String(fetchError), + errorType: fetchError instanceof Error ? fetchError.name : typeof fetchError, + url: deleteWebhookUrl, + stack: fetchError instanceof Error ? fetchError.stack : undefined + }); + throw fetchError; + } if (!response.ok) { const errorText = await response.text(); @@ -157,19 +186,40 @@ export class N8nService { }); logger.debug('Using webhook URL', { url: this.webhookUrl }); - logger.debug('API key present', { present: !!this.apiKey }); - - const response = await fetchWithTimeout(this.webhookUrl, { - method: 'POST', - timeout: 30000, // 30 seconds - headers: { - 'Content-Type': 'application/json', - 'x-api-key': this.apiKey - }, - body: JSON.stringify(cleanData), + logger.debug('API key present', { present: !!this.apiKey, length: this.apiKey ? this.apiKey.length : 0 }); + logger.debug('Request payload size', { size: JSON.stringify(cleanData).length }); + logger.debug('Request headers', { + contentType: 'application/json', + hasApiKey: !!this.apiKey, + apiKeyPrefix: this.apiKey ? this.apiKey.substring(0, 4) + '...' : 'none' }); - logger.debug('Webhook response', { status: response.status }); + let response: Response; + try { + response = await fetchWithTimeout(this.webhookUrl, { + method: 'POST', + timeout: 30000, // 30 seconds + headers: { + 'Content-Type': 'application/json', + 'x-api-key': this.apiKey + }, + body: JSON.stringify(cleanData), + }); + + logger.debug('Webhook response received', { + status: response.status, + statusText: response.statusText, + headers: Object.fromEntries(response.headers.entries()) + }); + } catch (fetchError) { + logger.error('Failed to send webhook request', { + error: fetchError instanceof Error ? fetchError.message : String(fetchError), + errorType: fetchError instanceof Error ? fetchError.name : typeof fetchError, + url: this.webhookUrl, + stack: fetchError instanceof Error ? fetchError.stack : undefined + }); + throw fetchError; + } if (!response.ok) { const errorText = await response.text();