missions finition

This commit is contained in:
alma 2026-01-21 13:02:42 +01:00
parent 1638dfec50
commit 680abdbc54
5 changed files with 131 additions and 33 deletions

View File

@ -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 n8nService = new N8nService();
const n8nData = { const n8nData = {
missionId: mission.id, missionId: mission.id,
@ -216,13 +224,34 @@ export async function DELETE(
penpotProjectId: mission.penpotProjectId penpotProjectId: mission.penpotProjectId
}; };
// Call N8N but don't fail if it errors logger.debug('Calling N8N deletion webhook', {
n8nService.triggerMissionDeletion(n8nData).catch(n8nError => { webhookUrl: process.env.N8N_DELETE_WEBHOOK_URL || 'https://brain.slm-lab.net/webhook/mission-delete',
logger.error('N8N deletion webhook failed (mission still deleted from database)', { hasApiKey: !!process.env.N8N_API_KEY,
error: n8nError instanceof Error ? n8nError.message : String(n8nError), 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 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) // Step 4: Delete from database (cascade will handle related records)
await prisma.mission.delete({ await prisma.mission.delete({
@ -236,13 +265,23 @@ export async function DELETE(
message: 'Mission deleted successfully' message: 'Mission deleted successfully'
}); });
} catch (error: any) { } 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', { logger.error('Error deleting mission', {
error: error instanceof Error ? error.message : String(error), error: error instanceof Error ? error.message : String(error),
errorStack: error instanceof Error ? error.stack : undefined,
missionId: errorMissionId missionId: errorMissionId
}); });
return NextResponse.json( 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 } { status: 500 }
); );
} }

View File

@ -496,16 +496,23 @@ export async function POST(request: Request) {
MISSION_API_URL: process.env.NEXT_PUBLIC_API_URL MISSION_API_URL: process.env.NEXT_PUBLIC_API_URL
} }
}; };
// Log detailed information for debugging
logger.debug('Sending to N8N', { logger.debug('Sending to N8N', {
missionId: n8nData.missionId, missionId: n8nData.missionId,
name: n8nData.name, 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); const workflowResult = await n8nService.triggerMissionCreation(n8nData);
logger.debug('N8N workflow result', { logger.debug('N8N workflow result', {
success: workflowResult.success, success: workflowResult.success,
hasError: !!workflowResult.error hasError: !!workflowResult.error,
error: workflowResult.error
}); });
if (!workflowResult.success) { if (!workflowResult.success) {

View File

@ -161,6 +161,7 @@ services:
# N8N Integration (required for mission creation workflow) # N8N Integration (required for mission creation workflow)
N8N_API_KEY: ${N8N_API_KEY} N8N_API_KEY: ${N8N_API_KEY}
N8N_WEBHOOK_URL: ${N8N_WEBHOOK_URL:-https://brain.slm-lab.net/webhook/mission-created} 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) # Autres variables d'environnement (ajoutez les vôtres)
# volumes: # volumes:

View File

@ -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 est utilisé pour créer automatiquement les projets Leantime, repos Git, etc.
N8N_API_KEY=VOTRE_N8N_API_KEY N8N_API_KEY=VOTRE_N8N_API_KEY
N8N_WEBHOOK_URL=https://brain.slm-lab.net/webhook/mission-created 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) # Autres services (ajoutez selon vos besoins)

View File

@ -13,6 +13,12 @@ export class N8nService {
this.rollbackWebhookUrl = process.env.N8N_ROLLBACK_WEBHOOK_URL || 'https://brain.slm-lab.net/webhook/mission-rollback'; this.rollbackWebhookUrl = process.env.N8N_ROLLBACK_WEBHOOK_URL || 'https://brain.slm-lab.net/webhook/mission-rollback';
this.apiKey = process.env.N8N_API_KEY || ''; 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) { if (!this.apiKey) {
logger.error('N8N_API_KEY is not set in environment variables'); 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', { logger.debug('Triggering n8n mission deletion workflow', {
missionId: data.missionId, missionId: data.missionId,
name: data.name, name: data.name,
hasRepoName: !!data.repoName hasRepoName: !!data.repoName,
repoName: data.repoName
}); });
logger.debug('Using deletion webhook URL', { url: deleteWebhookUrl }); logger.debug('Using deletion webhook URL', { url: deleteWebhookUrl });
logger.debug('API key present', { present: !!this.apiKey }); logger.debug('API key present', {
present: !!this.apiKey,
const response = await fetchWithTimeout(deleteWebhookUrl, { length: this.apiKey ? this.apiKey.length : 0
method: 'POST', });
timeout: 30000, // 30 seconds logger.debug('Request payload', {
headers: { keys: Object.keys(data),
'Content-Type': 'application/json', payloadSize: JSON.stringify(data).length
'x-api-key': this.apiKey
},
body: JSON.stringify(data),
}); });
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) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
@ -157,19 +186,40 @@ export class N8nService {
}); });
logger.debug('Using webhook URL', { url: this.webhookUrl }); logger.debug('Using webhook URL', { url: this.webhookUrl });
logger.debug('API key present', { present: !!this.apiKey }); 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 });
const response = await fetchWithTimeout(this.webhookUrl, { logger.debug('Request headers', {
method: 'POST', contentType: 'application/json',
timeout: 30000, // 30 seconds hasApiKey: !!this.apiKey,
headers: { apiKeyPrefix: this.apiKey ? this.apiKey.substring(0, 4) + '...' : 'none'
'Content-Type': 'application/json',
'x-api-key': this.apiKey
},
body: JSON.stringify(cleanData),
}); });
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) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();