Pages corrections widget

This commit is contained in:
alma 2026-01-16 18:22:20 +01:00
parent 5ba4d9ee7a
commit 7682eb07da
6 changed files with 1245 additions and 1 deletions

View File

@ -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

View File

@ -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<T>(fn: () => Promise<T>): Promise<T> {
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)

290
PRODUCTION_FIXES_APPLIED.md Normal file
View File

@ -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<MyType>('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)

View File

@ -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<Response>
* @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<Response> {
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<T> - Parsed JSON response
*
* @example
* ```typescript
* const data = await fetchJsonWithTimeout('https://api.example.com/data', {
* method: 'GET',
* timeout: 10000,
* });
* ```
*/
export async function fetchJsonWithTimeout<T = any>(
url: string,
options: FetchWithTimeoutOptions = {}
): Promise<T> {
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();
}

View File

@ -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"
},

161
scripts/validate-env.ts Normal file
View File

@ -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 };