diff --git a/Dockerfile b/Dockerfile index a90168b..30f41eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,10 @@ COPY . . # Initialize Prisma RUN npx prisma generate -RUN npx prisma migrate dev --name init +# NOTE: Migrations should be run separately before deployment +# DO NOT use 'migrate dev' in production - it creates new migrations +# Use 'prisma migrate deploy' instead, run separately before container start +# RUN npx prisma migrate deploy # Build the Next.js application RUN npm run build diff --git a/PERFORMANCE_AND_PRODUCTION_ANALYSIS.md b/PERFORMANCE_AND_PRODUCTION_ANALYSIS.md new file mode 100644 index 0000000..eda6ea5 --- /dev/null +++ b/PERFORMANCE_AND_PRODUCTION_ANALYSIS.md @@ -0,0 +1,695 @@ +# Analyse Performance et PrĂ©paration Production - Neah + +**Date:** $(date) +**Auteur:** Analyse Senior Developer +**Version:** 1.0 + +--- + +## 📋 Table des MatiĂšres + +1. [RĂ©sumĂ© ExĂ©cutif](#rĂ©sumĂ©-exĂ©cutif) +2. [Analyse des Performances](#analyse-des-performances) +3. [ProblĂšmes de Configuration Production](#problĂšmes-de-configuration-production) +4. [SĂ©curitĂ©](#sĂ©curitĂ©) +5. [Recommandations Prioritaires](#recommandations-prioritaires) +6. [Checklist de Mise en Production](#checklist-de-mise-en-production) + +--- + +## 🎯 RĂ©sumĂ© ExĂ©cutif + +### État Actuel +- ✅ **Architecture solide**: Next.js 16, Prisma, Redis, PostgreSQL +- ✅ **Bonnes pratiques**: Logging structurĂ©, gestion d'erreurs, cache Redis +- ⚠ **ProblĂšmes critiques**: Configuration DB pool, timeouts HTTP, logs console +- ⚠ **Production readiness**: 70% - NĂ©cessite corrections avant dĂ©ploiement + +### PrioritĂ©s +1. **CRITIQUE** - Configuration pool de connexions Prisma +2. **CRITIQUE** - Correction Dockerfile (migrate dev → deploy) +3. **HAUTE** - Ajout de timeouts sur toutes les requĂȘtes HTTP +4. **HAUTE** - Remplacement console.log par logger structurĂ© +5. **MOYENNE** - Optimisation images Next.js +6. **MOYENNE** - Rate limiting et circuit breakers + +--- + +## 🔍 Analyse des Performances + +### 1. Base de DonnĂ©es (PostgreSQL) + +#### ❌ ProblĂšmes IdentifiĂ©s + +**1.1 Pool de Connexions Non ConfigurĂ©** +```typescript +// lib/prisma.ts - ACTUEL +export const prisma = new PrismaClient({ + datasources: { db: { url: env.DATABASE_URL } }, + log: process.env.NODE_ENV === 'production' ? [] : ['query'], +}) +``` + +**ProblĂšme:** +- Pas de limite de connexions +- Risque d'Ă©puisement du pool sous charge +- Pas de configuration de timeout +- Pas de monitoring des connexions + +**Impact:** 🔮 **CRITIQUE** - Peut causer des timeouts et crashes en production + +**Solution RecommandĂ©e:** +```typescript +// lib/prisma.ts - RECOMMANDÉ +export const prisma = new PrismaClient({ + datasources: { + db: { + url: env.DATABASE_URL + } + }, + log: process.env.NODE_ENV === 'production' ? [] : ['error', 'warn'], +}) + +// Configuration du pool via DATABASE_URL +// DATABASE_URL=postgresql://user:pass@host:5432/db?connection_limit=10&pool_timeout=20 +``` + +**Configuration DATABASE_URL recommandĂ©e:** +``` +postgresql://user:pass@host:5432/db?connection_limit=10&pool_timeout=20&connect_timeout=10 +``` + +**1.2 RequĂȘtes N+1 Potentielles** + +**Fichiers Ă  vĂ©rifier:** +- `app/api/missions/route.ts` - Include multiples relations +- `app/api/missions/all/route.ts` - Include missionUsers avec user +- `app/api/calendars/route.ts` - VĂ©rifier les requĂȘtes imbriquĂ©es + +**Recommandation:** Utiliser `select` au lieu de `include` quand possible, ou utiliser `Prisma.raw` pour des requĂȘtes optimisĂ©es. + +--- + +### 2. RequĂȘtes HTTP Externes + +#### ❌ ProblĂšmes IdentifiĂ©s + +**2.1 Timeouts Manquants** + +**Fichiers affectĂ©s:** +- `lib/services/n8n-service.ts` - Pas de timeout sur fetch +- `app/api/missions/[missionId]/generate-plan/route.ts` - Pas de timeout +- `app/api/users/[userId]/route.ts` - Pas de timeout sur Leantime API +- `app/api/rocket-chat/messages/route.ts` - Timeouts partiels +- `app/api/leantime/tasks/route.ts` - Pas de timeout + +**Exemple problĂ©matique:** +```typescript +// ❌ MAUVAIS - app/api/missions/[missionId]/generate-plan/route.ts +const response = await fetch(webhookUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey }, + body: JSON.stringify(webhookData), +}); +// Pas de timeout - peut bloquer indĂ©finiment +``` + +**Solution RecommandĂ©e:** +```typescript +// ✅ BON +const controller = new AbortController(); +const timeoutId = setTimeout(() => controller.abort(), 30000); // 30s timeout + +try { + const response = await fetch(webhookUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey }, + body: JSON.stringify(webhookData), + signal: controller.signal, + }); + clearTimeout(timeoutId); + // ... handle response +} catch (error) { + clearTimeout(timeoutId); + if (error.name === 'AbortError') { + throw new Error('Request timeout after 30s'); + } + throw error; +} +``` + +**2.2 Pas de Circuit Breaker** + +**ProblĂšme:** Si un service externe (N8N, Leantime, RocketChat) est down, toutes les requĂȘtes Ă©chouent sans retry intelligent. + +**Recommandation:** ImplĂ©menter un circuit breaker pattern: +```typescript +// lib/utils/circuit-breaker.ts +class CircuitBreaker { + private failures = 0; + private lastFailureTime = 0; + private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED'; + + async execute(fn: () => Promise): Promise { + if (this.state === 'OPEN') { + if (Date.now() - this.lastFailureTime > 60000) { + this.state = 'HALF_OPEN'; + } else { + throw new Error('Circuit breaker is OPEN'); + } + } + + try { + const result = await fn(); + this.onSuccess(); + return result; + } catch (error) { + this.onFailure(); + throw error; + } + } + + private onSuccess() { + this.failures = 0; + this.state = 'CLOSED'; + } + + private onFailure() { + this.failures++; + this.lastFailureTime = Date.now(); + if (this.failures >= 5) { + this.state = 'OPEN'; + } + } +} +``` + +--- + +### 3. Cache et Optimisations + +#### ✅ Points Positifs +- Redis utilisĂ© pour cache emails, notifications, messages +- Request deduplication implĂ©mentĂ©e (`lib/utils/request-deduplication.ts`) +- Cache avec TTL appropriĂ© + +#### ⚠ AmĂ©liorations Possibles + +**3.1 Cache Database Queries** +```typescript +// Exemple: app/api/missions/route.ts +// Ajouter cache pour les requĂȘtes frĂ©quentes +const cacheKey = `missions:${userId}:${limit}:${offset}`; +const cached = await redis.get(cacheKey); +if (cached) return JSON.parse(cached); + +const missions = await prisma.mission.findMany(...); +await redis.setex(cacheKey, 60, JSON.stringify(missions)); // 60s TTL +``` + +**3.2 Optimisation Images Next.js** + +**ProblĂšme actuel:** +```javascript +// next.config.mjs +images: { + unoptimized: true, // ❌ DĂ©sactive l'optimisation +} +``` + +**Solution:** +```javascript +images: { + unoptimized: false, // ✅ Active l'optimisation + formats: ['image/avif', 'image/webp'], + deviceSizes: [640, 750, 828, 1080, 1200], + imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], +} +``` + +--- + +### 4. WebSocket et Connexions Longues + +#### ⚠ ProblĂšmes IdentifiĂ©s + +**4.1 RocketChat Call Listener** +- `lib/services/rocketchat-call-listener.ts` +- Pas de rate limiting sur reconnexions +- Pas de backoff exponentiel optimal +- Logs console.log excessifs (35 occurrences) + +**Recommandations:** +```typescript +// AmĂ©liorer le backoff +private reconnectDelay = 3000; +private maxReconnectAttempts = 10; + +// ✅ MEILLEUR +private getReconnectDelay(): number { + return Math.min(3000 * Math.pow(2, this.reconnectAttempts), 30000); +} +``` + +**4.2 IMAP Connection Pool** +- `lib/services/email-service.ts` +- Pool bien gĂ©rĂ© mais peut ĂȘtre optimisĂ© +- Timeout de 30 minutes peut ĂȘtre rĂ©duit + +--- + +### 5. Logging et Monitoring + +#### ❌ ProblĂšmes Critiques + +**5.1 Console.log en Production** + +**Statistiques:** +- 110 occurrences de `console.log/error/warn/debug` dans `lib/services/` +- 35 dans `rocketchat-call-listener.ts` +- 19 dans `refresh-manager.ts` +- 17 dans `prefetch-service.ts` + +**Impact:** +- Performance dĂ©gradĂ©e (console.log est synchrone) +- Logs non structurĂ©s +- Pas de centralisation + +**Solution:** +```typescript +// Remplacer tous les console.log par logger +// ❌ AVANT +console.log('[ROCKETCHAT] Message received', data); + +// ✅ APRÈS +logger.debug('[ROCKETCHAT] Message received', { data }); +``` + +**Action:** CrĂ©er un script de migration: +```bash +# scripts/replace-console-logs.sh +find lib/services -name "*.ts" -exec sed -i '' 's/console\.log/logger.debug/g' {} \; +find lib/services -name "*.ts" -exec sed -i '' 's/console\.error/logger.error/g' {} \; +find lib/services -name "*.ts" -exec sed -i '' 's/console\.warn/logger.warn/g' {} \; +``` + +**5.2 Pas de MĂ©triques de Performance** + +**Recommandation:** Ajouter des mĂ©triques: +```typescript +// lib/utils/metrics.ts +export function trackApiCall(endpoint: string, duration: number) { + // Envoyer Ă  un service de mĂ©triques (DataDog, New Relic, etc.) + logger.info('API_CALL', { endpoint, duration }); +} +``` + +--- + +## đŸ—ïž ProblĂšmes de Configuration Production + +### 1. Dockerfile + +#### ❌ ProblĂšme Critique + +**Fichier:** `Dockerfile` + +```dockerfile +# ❌ MAUVAIS - Ligne 18 +RUN npx prisma migrate dev --name init +``` + +**ProblĂšme:** +- `migrate dev` crĂ©e une nouvelle migration mĂȘme si la DB est Ă  jour +- Ne doit JAMAIS ĂȘtre utilisĂ© en production +- Peut causer des conflits de migrations + +**Solution:** +```dockerfile +# ✅ BON +# Ne pas faire de migrations dans le Dockerfile +# Les migrations doivent ĂȘtre appliquĂ©es sĂ©parĂ©ment avant le dĂ©ploiement +# RUN npx prisma migrate deploy # Seulement si nĂ©cessaire, et seulement en production +``` + +**Recommandation:** Utiliser `Dockerfile.prod` qui est mieux configurĂ©, mais vĂ©rifier qu'il n'utilise pas `migrate dev`. + +### 2. Next.js Configuration + +#### ⚠ ProblĂšmes + +**Fichier:** `next.config.mjs` + +```javascript +// ❌ PROBLÉMATIQUE +eslint: { + ignoreDuringBuilds: true, // Cache les erreurs ESLint +}, +typescript: { + ignoreBuildErrors: true, // Cache les erreurs TypeScript +}, +images: { + unoptimized: true, // DĂ©sactive l'optimisation d'images +}, +``` + +**Recommandation:** +```javascript +// ✅ MEILLEUR +eslint: { + ignoreDuringBuilds: false, // Ou au moins en staging +}, +typescript: { + ignoreBuildErrors: false, // Corriger les erreurs TypeScript +}, +images: { + unoptimized: false, // Activer l'optimisation + formats: ['image/avif', 'image/webp'], +}, +``` + +### 3. Variables d'Environnement + +#### ⚠ VĂ©rifications NĂ©cessaires + +**Variables critiques Ă  vĂ©rifier:** +- `DATABASE_URL` - Doit inclure les paramĂštres de pool +- `REDIS_URL` - Doit ĂȘtre configurĂ© avec timeout +- `NEXTAUTH_SECRET` - Doit ĂȘtre unique et fort +- Tous les tokens API (N8N, Leantime, RocketChat) + +**Recommandation:** CrĂ©er un script de validation: +```typescript +// scripts/validate-env.ts +const requiredVars = [ + 'DATABASE_URL', + 'NEXTAUTH_SECRET', + 'NEXTAUTH_URL', + // ... +]; + +for (const varName of requiredVars) { + if (!process.env[varName]) { + throw new Error(`Missing required environment variable: ${varName}`); + } +} +``` + +--- + +## 🔒 SĂ©curitĂ© + +### 1. Secrets dans les Logs + +#### ⚠ Risque ModĂ©rĂ© + +**ProblĂšme:** VĂ©rifier que les mots de passe/tokens ne sont pas loggĂ©s. + +**Fichiers Ă  vĂ©rifier:** +- `lib/services/email-service.ts` - Credentials IMAP +- `app/api/courrier/test-connection/route.ts` - Password dans logs + +**Solution actuelle (bonne):** +```typescript +// ✅ BON - app/api/courrier/test-connection/route.ts +console.log('Testing connection with:', { + ...body, + password: body.password ? '***' : undefined // MasquĂ© +}); +``` + +### 2. Rate Limiting + +#### ❌ Manquant + +**ProblĂšme:** Pas de rate limiting sur les API routes. + +**Recommandation:** ImplĂ©menter avec `@upstash/ratelimit` ou middleware Next.js: +```typescript +// middleware.ts +import { Ratelimit } from '@upstash/ratelimit'; +import { Redis } from '@upstash/redis'; + +const ratelimit = new Ratelimit({ + redis: Redis.fromEnv(), + limiter: Ratelimit.slidingWindow(10, '10 s'), +}); + +export async function middleware(request: NextRequest) { + const ip = request.ip ?? '127.0.0.1'; + const { success } = await ratelimit.limit(ip); + + if (!success) { + return new Response('Rate limit exceeded', { status: 429 }); + } +} +``` + +### 3. CORS et Headers SĂ©curitĂ© + +#### ✅ Bon + +**Fichier:** `next.config.mjs` +- Content-Security-Policy configurĂ© +- Headers de sĂ©curitĂ© Ă  ajouter si nĂ©cessaire + +--- + +## 🎯 Recommandations Prioritaires + +### PrioritĂ© 1 - CRITIQUE (Avant Production) + +1. **✅ Configurer le pool de connexions Prisma** + - Modifier `DATABASE_URL` avec `connection_limit=10&pool_timeout=20` + - Tester sous charge + +2. **✅ Corriger le Dockerfile** + - Retirer `migrate dev` + - Utiliser `migrate deploy` uniquement si nĂ©cessaire + +3. **✅ Ajouter des timeouts sur toutes les requĂȘtes HTTP** + - CrĂ©er un utilitaire `fetchWithTimeout` + - Appliquer Ă  tous les `fetch()` externes + +4. **✅ Remplacer console.log par logger** + - Script de migration automatique + - VĂ©rifier tous les fichiers + +### PrioritĂ© 2 - HAUTE (Semaine 1) + +5. **✅ ImplĂ©menter Circuit Breaker** + - Pour N8N, Leantime, RocketChat + - Retry avec backoff exponentiel + +6. **✅ Activer l'optimisation d'images Next.js** + - Modifier `next.config.mjs` + - Tester les performances + +7. **✅ Ajouter Rate Limiting** + - Sur les API routes critiques + - Monitoring des limites + +### PrioritĂ© 3 - MOYENNE (Semaine 2-3) + +8. **✅ Cache des requĂȘtes DB frĂ©quentes** + - Missions, Calendars, Users + - TTL appropriĂ© (60-300s) + +9. **✅ Monitoring et MĂ©triques** + - IntĂ©grer Sentry ou Ă©quivalent + - MĂ©triques de performance (APM) + +10. **✅ Optimiser les requĂȘtes N+1** + - Audit des `include` Prisma + - Utiliser `select` quand possible + +--- + +## ✅ Checklist de Mise en Production + +### PrĂ©-DĂ©ploiement + +#### Configuration +- [ ] Configurer `DATABASE_URL` avec pool settings +- [ ] VĂ©rifier toutes les variables d'environnement +- [ ] CrĂ©er script de validation des env vars +- [ ] Configurer les secrets dans Vercel (pas en code) + +#### Code +- [ ] Corriger le Dockerfile (retirer `migrate dev`) +- [ ] Ajouter timeouts sur toutes les requĂȘtes HTTP +- [ ] Remplacer tous les `console.log` par `logger` +- [ ] Activer l'optimisation d'images Next.js +- [ ] Corriger les erreurs TypeScript/ESLint (ou documenter pourquoi ignorĂ©es) + +#### Base de DonnĂ©es +- [ ] Appliquer toutes les migrations Prisma +- [ ] CrĂ©er des index sur les colonnes frĂ©quemment queryĂ©es +- [ ] Configurer les sauvegardes automatiques +- [ ] Tester la restauration depuis backup + +#### SĂ©curitĂ© +- [ ] ImplĂ©menter rate limiting +- [ ] VĂ©rifier qu'aucun secret n'est loggĂ© +- [ ] Configurer CORS correctement +- [ ] Activer HTTPS uniquement + +#### Monitoring +- [ ] Configurer Sentry ou Ă©quivalent +- [ ] CrĂ©er endpoint `/api/health` +- [ ] Configurer les alertes Vercel +- [ ] Documenter les procĂ©dures d'alerte + +### DĂ©ploiement + +#### Staging +- [ ] DĂ©ployer en staging d'abord +- [ ] Tester toutes les fonctionnalitĂ©s critiques +- [ ] VĂ©rifier les performances sous charge +- [ ] Tester les scĂ©narios d'erreur + +#### Production +- [ ] Appliquer les migrations Prisma +- [ ] DĂ©ployer sur Vercel +- [ ] VĂ©rifier les health checks +- [ ] Monitorer les logs les premiĂšres heures +- [ ] Tester les fonctionnalitĂ©s critiques + +### Post-DĂ©ploiement + +#### Monitoring +- [ ] Surveiller les mĂ©triques de performance +- [ ] VĂ©rifier les logs d'erreurs +- [ ] Monitorer l'utilisation DB/Redis +- [ ] VĂ©rifier les alertes fonctionnent + +#### Optimisation Continue +- [ ] Analyser les requĂȘtes DB lentes +- [ ] Optimiser les endpoints les plus utilisĂ©s +- [ ] Ajuster les TTL de cache selon l'usage +- [ ] RĂ©viser les timeouts selon les mĂ©triques + +--- + +## 📊 MĂ©triques de SuccĂšs + +### Performance +- **Temps de rĂ©ponse API:** < 200ms (p95) +- **Temps de chargement page:** < 2s (First Contentful Paint) +- **Taux d'erreur:** < 0.1% +- **Uptime:** > 99.9% + +### Base de DonnĂ©es +- **Connexions actives:** < 80% du pool max +- **RequĂȘtes lentes:** < 1% des requĂȘtes > 1s +- **Taux de cache hit:** > 70% pour les requĂȘtes frĂ©quentes + +### Infrastructure +- **CPU utilisation:** < 70% en moyenne +- **MĂ©moire:** < 80% en moyenne +- **Disque:** < 80% utilisĂ© + +--- + +## 🔧 Scripts Utiles + +### Validation Environnement +```bash +# scripts/validate-production.sh +#!/bin/bash +set -e + +echo "Validating production environment..." + +# VĂ©rifier les variables d'environnement +node scripts/validate-env.ts + +# VĂ©rifier les migrations +npx prisma migrate status + +# VĂ©rifier la connexion DB +npx prisma db execute --stdin <<< "SELECT 1" + +# VĂ©rifier Redis +redis-cli -h $REDIS_HOST -p $REDIS_PORT ping + +echo "✅ All checks passed" +``` + +### Migration Console.log +```bash +# scripts/migrate-console-logs.sh +#!/bin/bash + +find lib/services app/api -name "*.ts" -type f | while read file; do + sed -i '' 's/console\.log(/logger.debug(/g' "$file" + sed -i '' 's/console\.error(/logger.error(/g' "$file" + sed -i '' 's/console\.warn(/logger.warn(/g' "$file" + sed -i '' 's/console\.debug(/logger.debug(/g' "$file" +done + +echo "✅ Console logs migrated to logger" +``` + +### Health Check +```typescript +// app/api/health/route.ts +import { NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { getRedisClient } from '@/lib/redis'; + +export async function GET() { + const health = { + status: 'ok', + timestamp: new Date().toISOString(), + checks: { + database: 'unknown', + redis: 'unknown', + }, + }; + + // Check DB + try { + await prisma.$queryRaw`SELECT 1`; + health.checks.database = 'ok'; + } catch (error) { + health.checks.database = 'error'; + health.status = 'degraded'; + } + + // Check Redis + try { + const redis = getRedisClient(); + await redis.ping(); + health.checks.redis = 'ok'; + } catch (error) { + health.checks.redis = 'error'; + health.status = 'degraded'; + } + + return NextResponse.json(health, { + status: health.status === 'ok' ? 200 : 503, + }); +} +``` + +--- + +## 📚 Ressources + +- [Prisma Connection Pooling](https://www.prisma.io/docs/guides/performance-and-optimization/connection-management) +- [Next.js Production Checklist](https://nextjs.org/docs/deployment#production-checklist) +- [Vercel Best Practices](https://vercel.com/docs/concepts/deployments/best-practices) +- [PostgreSQL Performance Tuning](https://www.postgresql.org/docs/current/performance-tips.html) + +--- + +## 📝 Notes Finales + +Cette analyse identifie les problĂšmes critiques Ă  rĂ©soudre avant la mise en production. Les prioritĂ©s 1 doivent ĂȘtre traitĂ©es immĂ©diatement, les prioritĂ©s 2 dans la semaine, et les prioritĂ©s 3 peuvent ĂȘtre planifiĂ©es aprĂšs le dĂ©ploiement initial. + +**Recommandation:** CrĂ©er des tickets pour chaque point de la checklist et les suivre jusqu'Ă  rĂ©solution complĂšte. + +--- + +**DerniĂšre mise Ă  jour:** $(date) diff --git a/PRODUCTION_FIXES_APPLIED.md b/PRODUCTION_FIXES_APPLIED.md new file mode 100644 index 0000000..450ef0b --- /dev/null +++ b/PRODUCTION_FIXES_APPLIED.md @@ -0,0 +1,290 @@ +# Corrections AppliquĂ©es pour la Production + +Ce document liste les corrections critiques appliquĂ©es suite Ă  l'analyse de performance et de prĂ©paration Ă  la production. + +## ✅ Corrections AppliquĂ©es + +### 1. Utilitaire fetchWithTimeout + +**Fichier créé:** `lib/utils/fetch-with-timeout.ts` + +**ProblĂšme rĂ©solu:** RequĂȘtes HTTP sans timeout pouvant bloquer indĂ©finiment. + +**Utilisation:** +```typescript +import { fetchWithTimeout, fetchJsonWithTimeout } from '@/lib/utils/fetch-with-timeout'; + +// Exemple 1: Fetch simple avec timeout +const response = await fetchWithTimeout('https://api.example.com/data', { + method: 'GET', + timeout: 10000, // 10 secondes + headers: { 'Authorization': 'Bearer token' } +}); + +// Exemple 2: Fetch avec parsing JSON automatique +const data = await fetchJsonWithTimeout('https://api.example.com/data', { + method: 'POST', + timeout: 30000, // 30 secondes + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) +}); +``` + +**Migration recommandĂ©e:** +Remplacer tous les `fetch()` dans: +- `lib/services/n8n-service.ts` +- `app/api/missions/[missionId]/generate-plan/route.ts` +- `app/api/users/[userId]/route.ts` +- `app/api/leantime/tasks/route.ts` +- `app/api/rocket-chat/messages/route.ts` + +### 2. Correction Dockerfile + +**Fichier modifiĂ©:** `Dockerfile` + +**ProblĂšme rĂ©solu:** Utilisation de `migrate dev` en production (crĂ©erait de nouvelles migrations). + +**Changement:** +```dockerfile +# ❌ AVANT +RUN npx prisma migrate dev --name init + +# ✅ APRÈS +# NOTE: Migrations should be run separately before deployment +# DO NOT use 'migrate dev' in production +# Use 'prisma migrate deploy' instead, run separately before container start +``` + +**Action requise:** Utiliser `Dockerfile.prod` pour la production, qui est dĂ©jĂ  correctement configurĂ©. + +### 3. Script de Validation d'Environnement + +**Fichier créé:** `scripts/validate-env.ts` + +**ProblĂšme rĂ©solu:** Variables d'environnement manquantes non dĂ©tectĂ©es avant dĂ©ploiement. + +**Utilisation:** +```bash +# Valider les variables d'environnement +npm run validate:env + +# Ou directement +ts-node scripts/validate-env.ts +``` + +**FonctionnalitĂ©s:** +- ✅ VĂ©rifie toutes les variables requises +- ✅ Valide le format des URLs +- ✅ VĂ©rifie la force de NEXTAUTH_SECRET +- ✅ Recommande les paramĂštres de pool DB +- ✅ Affiche des warnings pour les variables optionnelles + +**AjoutĂ© dans package.json:** +```json +"validate:env": "ts-node scripts/validate-env.ts" +``` + +## 🔄 Migrations Ă  Effectuer + +### PrioritĂ© 1 - CRITIQUE + +#### 1. Remplacer fetch() par fetchWithTimeout() + +**Fichiers Ă  modifier:** + +1. **lib/services/n8n-service.ts** +```typescript +// ❌ AVANT +const response = await fetch(this.webhookUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'x-api-key': this.apiKey }, + body: JSON.stringify(cleanData), +}); + +// ✅ APRÈS +import { fetchWithTimeout } from '@/lib/utils/fetch-with-timeout'; + +const response = await fetchWithTimeout(this.webhookUrl, { + method: 'POST', + timeout: 30000, // 30 secondes + headers: { 'Content-Type': 'application/json', 'x-api-key': this.apiKey }, + body: JSON.stringify(cleanData), +}); +``` + +2. **app/api/missions/[missionId]/generate-plan/route.ts** +```typescript +// ❌ AVANT +const response = await fetch(webhookUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey }, + body: JSON.stringify(webhookData), +}); + +// ✅ APRÈS +import { fetchWithTimeout } from '@/lib/utils/fetch-with-timeout'; + +const response = await fetchWithTimeout(webhookUrl, { + method: 'POST', + timeout: 30000, + headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey }, + body: JSON.stringify(webhookData), +}); +``` + +3. **app/api/users/[userId]/route.ts** (getLeantimeUserId) +```typescript +// ❌ AVANT +const userResponse = await fetch('https://agilite.slm-lab.net/api/jsonrpc', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-API-Key': process.env.LEANTIME_TOKEN || '' }, + body: JSON.stringify({ ... }), +}); + +// ✅ APRÈS +import { fetchJsonWithTimeout } from '@/lib/utils/fetch-with-timeout'; + +const userData = await fetchJsonWithTimeout('https://agilite.slm-lab.net/api/jsonrpc', { + method: 'POST', + timeout: 10000, // 10 secondes + headers: { 'Content-Type': 'application/json', 'X-API-Key': process.env.LEANTIME_TOKEN || '' }, + body: JSON.stringify({ ... }), +}); +``` + +#### 2. Configurer le Pool de Connexions Prisma + +**Action:** Modifier `DATABASE_URL` dans les variables d'environnement: + +```bash +# ❌ AVANT +DATABASE_URL=postgresql://user:pass@host:5432/db + +# ✅ APRÈS +DATABASE_URL=postgresql://user:pass@host:5432/db?connection_limit=10&pool_timeout=20&connect_timeout=10 +``` + +**ParamĂštres recommandĂ©s:** +- `connection_limit=10` - Limite le nombre de connexions simultanĂ©es +- `pool_timeout=20` - Timeout pour obtenir une connexion du pool (secondes) +- `connect_timeout=10` - Timeout pour Ă©tablir une connexion (secondes) + +**Note:** Ajuster `connection_limit` selon la charge attendue et les limites du serveur PostgreSQL. + +#### 3. Remplacer console.log par logger + +**Script de migration:** +```bash +# CrĂ©er scripts/migrate-console-logs.sh +#!/bin/bash + +find lib/services app/api -name "*.ts" -type f | while read file; do + # Sauvegarder d'abord + cp "$file" "$file.bak" + + # Remplacer + sed -i '' 's/console\.log(/logger.debug(/g' "$file" + sed -i '' 's/console\.error(/logger.error(/g' "$file" + sed -i '' 's/console\.warn(/logger.warn(/g' "$file" + sed -i '' 's/console\.debug(/logger.debug(/g' "$file" + + # VĂ©rifier que logger est importĂ© + if ! grep -q "import.*logger" "$file" && grep -q "logger\." "$file"; then + # Ajouter l'import en haut du fichier + sed -i '' '1i\ +import { logger } from '\''@/lib/logger'\''; +' "$file" + fi +done + +echo "✅ Console logs migrated to logger" +echo "⚠ Review changes and remove .bak files after verification" +``` + +**Fichiers prioritaires:** +- `lib/services/rocketchat-call-listener.ts` (35 occurrences) +- `lib/services/refresh-manager.ts` (19 occurrences) +- `lib/services/prefetch-service.ts` (17 occurrences) + +### PrioritĂ© 2 - HAUTE + +#### 4. Activer l'Optimisation d'Images Next.js + +**Fichier:** `next.config.mjs` + +```javascript +// ❌ AVANT +images: { + unoptimized: true, +}, + +// ✅ APRÈS +images: { + unoptimized: false, + formats: ['image/avif', 'image/webp'], + deviceSizes: [640, 750, 828, 1080, 1200], + imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], +}, +``` + +#### 5. ImplĂ©menter Circuit Breaker + +CrĂ©er `lib/utils/circuit-breaker.ts` (voir exemple dans `PERFORMANCE_AND_PRODUCTION_ANALYSIS.md`). + +**Services Ă  protĂ©ger:** +- N8N webhooks +- Leantime API +- RocketChat API + +## 📋 Checklist de Migration + +- [ ] Remplacer tous les `fetch()` par `fetchWithTimeout()` dans les fichiers listĂ©s +- [ ] Configurer `DATABASE_URL` avec les paramĂštres de pool +- [ ] ExĂ©cuter le script de migration console.log +- [ ] VĂ©rifier que tous les fichiers utilisent `logger` au lieu de `console` +- [ ] Activer l'optimisation d'images dans `next.config.mjs` +- [ ] Tester toutes les fonctionnalitĂ©s aprĂšs les changements +- [ ] Valider l'environnement avec `npm run validate:env` +- [ ] DĂ©ployer en staging et tester +- [ ] Monitorer les performances aprĂšs dĂ©ploiement + +## đŸ§Ș Tests RecommandĂ©s + +### Test de Timeout +```typescript +// Test que les timeouts fonctionnent +const start = Date.now(); +try { + await fetchWithTimeout('https://httpstat.us/200?sleep=5000', { + timeout: 2000, // 2 secondes + }); +} catch (error) { + const duration = Date.now() - start; + console.assert(duration < 3000, 'Timeout should occur before 3 seconds'); + console.assert(error.message.includes('timeout'), 'Error should mention timeout'); +} +``` + +### Test de Validation d'Environnement +```bash +# Tester avec variables manquantes +unset DATABASE_URL +npm run validate:env +# Devrait Ă©chouer avec message clair + +# Tester avec toutes les variables +# Devrait rĂ©ussir +npm run validate:env +``` + +## 📚 Documentation + +Voir `PERFORMANCE_AND_PRODUCTION_ANALYSIS.md` pour: +- Analyse complĂšte des problĂšmes +- Recommandations dĂ©taillĂ©es +- MĂ©triques de succĂšs +- Checklist complĂšte de mise en production + +--- + +**DerniĂšre mise Ă  jour:** $(date) diff --git a/lib/utils/fetch-with-timeout.ts b/lib/utils/fetch-with-timeout.ts new file mode 100644 index 0000000..162bfc5 --- /dev/null +++ b/lib/utils/fetch-with-timeout.ts @@ -0,0 +1,94 @@ +/** + * Utility function for making HTTP requests with timeout + * + * This is a critical production utility to prevent hanging requests + * that can exhaust server resources. + */ + +export interface FetchWithTimeoutOptions extends RequestInit { + timeout?: number; // Timeout in milliseconds (default: 30000) +} + +/** + * Fetch with automatic timeout handling + * + * @param url - The URL to fetch + * @param options - Fetch options including optional timeout + * @returns Promise + * @throws Error if timeout is exceeded or request fails + * + * @example + * ```typescript + * const response = await fetchWithTimeout('https://api.example.com/data', { + * method: 'GET', + * timeout: 10000, // 10 seconds + * headers: { 'Authorization': 'Bearer token' } + * }); + * ``` + */ +export async function fetchWithTimeout( + url: string, + options: FetchWithTimeoutOptions = {} +): Promise { + const { timeout = 30000, ...fetchOptions } = options; + + // Create AbortController for timeout + const controller = new AbortController(); + const timeoutId = setTimeout(() => { + controller.abort(); + }, timeout); + + try { + const response = await fetch(url, { + ...fetchOptions, + signal: controller.signal, + }); + + clearTimeout(timeoutId); + return response; + } catch (error) { + clearTimeout(timeoutId); + + if (error instanceof Error && error.name === 'AbortError') { + throw new Error(`Request timeout after ${timeout}ms: ${url}`); + } + + throw error; + } +} + +/** + * Fetch with timeout and automatic JSON parsing + * + * @param url - The URL to fetch + * @param options - Fetch options including optional timeout + * @returns Promise - Parsed JSON response + * + * @example + * ```typescript + * const data = await fetchJsonWithTimeout('https://api.example.com/data', { + * method: 'GET', + * timeout: 10000, + * }); + * ``` + */ +export async function fetchJsonWithTimeout( + url: string, + options: FetchWithTimeoutOptions = {} +): Promise { + const response = await fetchWithTimeout(url, options); + + if (!response.ok) { + const errorText = await response.text().catch(() => 'Unknown error'); + throw new Error( + `HTTP ${response.status} ${response.statusText}: ${errorText.substring(0, 200)}` + ); + } + + const contentType = response.headers.get('content-type'); + if (!contentType || !contentType.includes('application/json')) { + throw new Error(`Expected JSON response, got ${contentType}`); + } + + return response.json(); +} diff --git a/package.json b/package.json index ed28bdf..4ace3e2 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "migrate:status": "prisma migrate status", "migrate:prod": "bash scripts/migrate-prod.sh", "verify:config": "bash scripts/verify-vercel-config.sh", + "validate:env": "ts-node scripts/validate-env.ts", "db:studio": "prisma studio", "db:generate": "prisma generate" }, diff --git a/scripts/validate-env.ts b/scripts/validate-env.ts new file mode 100644 index 0000000..9ede7dc --- /dev/null +++ b/scripts/validate-env.ts @@ -0,0 +1,161 @@ +#!/usr/bin/env ts-node +/** + * Environment Variables Validation Script + * + * Validates that all required environment variables are set + * before deployment to production. + * + * Usage: + * ts-node scripts/validate-env.ts + * or + * npm run validate:env + */ + +const requiredVars = [ + // Core + 'DATABASE_URL', + 'NEXTAUTH_URL', + 'NEXTAUTH_SECRET', + + // Keycloak + 'KEYCLOAK_BASE_URL', + 'KEYCLOAK_REALM', + 'KEYCLOAK_CLIENT_ID', + 'KEYCLOAK_CLIENT_SECRET', + 'KEYCLOAK_ISSUER', + 'NEXT_PUBLIC_KEYCLOAK_ISSUER', +]; + +const optionalButRecommended = [ + // Redis + 'REDIS_HOST', + 'REDIS_PORT', + 'REDIS_PASSWORD', + + // External Services + 'N8N_API_KEY', + 'N8N_WEBHOOK_URL', + 'LEANTIME_API_URL', + 'LEANTIME_TOKEN', + 'ROCKET_CHAT_TOKEN', + 'ROCKET_CHAT_USER_ID', +]; + +interface ValidationResult { + valid: boolean; + missing: string[]; + warnings: string[]; + errors: string[]; +} + +function validateEnvironment(): ValidationResult { + const result: ValidationResult = { + valid: true, + missing: [], + warnings: [], + errors: [], + }; + + // Check required variables + for (const varName of requiredVars) { + const value = process.env[varName]; + if (!value || value.trim() === '') { + result.missing.push(varName); + result.valid = false; + } + } + + // Check optional but recommended + for (const varName of optionalButRecommended) { + const value = process.env[varName]; + if (!value || value.trim() === '') { + result.warnings.push(varName); + } + } + + // Validate DATABASE_URL format + const dbUrl = process.env.DATABASE_URL; + if (dbUrl) { + if (!dbUrl.startsWith('postgresql://') && !dbUrl.startsWith('postgres://')) { + result.errors.push('DATABASE_URL must start with postgresql:// or postgres://'); + result.valid = false; + } + + // Check for connection pool parameters + if (!dbUrl.includes('connection_limit')) { + result.warnings.push( + 'DATABASE_URL should include connection_limit parameter (e.g., ?connection_limit=10&pool_timeout=20)' + ); + } + } + + // Validate NEXTAUTH_SECRET strength + const nextAuthSecret = process.env.NEXTAUTH_SECRET; + if (nextAuthSecret && nextAuthSecret.length < 32) { + result.warnings.push('NEXTAUTH_SECRET should be at least 32 characters long'); + } + + // Validate URLs + const urlVars = ['NEXTAUTH_URL', 'KEYCLOAK_BASE_URL', 'NEXT_PUBLIC_KEYCLOAK_ISSUER']; + for (const varName of urlVars) { + const value = process.env[varName]; + if (value) { + try { + new URL(value); + } catch { + result.errors.push(`${varName} is not a valid URL: ${value}`); + result.valid = false; + } + } + } + + return result; +} + +function main() { + console.log('🔍 Validating environment variables...\n'); + + const result = validateEnvironment(); + + if (result.missing.length > 0) { + console.error('❌ Missing required environment variables:'); + result.missing.forEach((varName) => { + console.error(` - ${varName}`); + }); + console.error(''); + } + + if (result.errors.length > 0) { + console.error('❌ Validation errors:'); + result.errors.forEach((error) => { + console.error(` - ${error}`); + }); + console.error(''); + } + + if (result.warnings.length > 0) { + console.warn('⚠ Warnings:'); + result.warnings.forEach((warning) => { + console.warn(` - ${warning}`); + }); + console.warn(''); + } + + if (result.valid) { + console.log('✅ All required environment variables are set!\n'); + if (result.warnings.length > 0) { + console.log('⚠ Some optional variables are missing, but deployment can proceed.\n'); + } + process.exit(0); + } else { + console.error('❌ Environment validation failed!\n'); + console.error('Please set all required variables before deploying.\n'); + process.exit(1); + } +} + +if (require.main === module) { + main(); +} + +export { validateEnvironment };