diff --git a/package.json b/package.json index c8ae805..ad1b5b5 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,11 @@ "backup:config": "npx tsx scripts/backup-manager.ts config", "backup:start": "npx tsx scripts/backup-manager.ts scheduler-start", "backup:stop": "npx tsx scripts/backup-manager.ts scheduler-stop", - "backup:status": "npx tsx scripts/backup-manager.ts scheduler-status" + "backup:status": "npx tsx scripts/backup-manager.ts scheduler-status", + "cache:monitor": "npx tsx scripts/cache-monitor.ts", + "cache:stats": "npx tsx scripts/cache-monitor.ts stats", + "cache:cleanup": "npx tsx scripts/cache-monitor.ts cleanup", + "cache:clear": "npx tsx scripts/cache-monitor.ts clear" }, "dependencies": { "@dnd-kit/core": "^6.3.1", diff --git a/scripts/cache-monitor.ts b/scripts/cache-monitor.ts new file mode 100644 index 0000000..c6115fd --- /dev/null +++ b/scripts/cache-monitor.ts @@ -0,0 +1,137 @@ +#!/usr/bin/env tsx + +/** + * Script de monitoring du cache Jira Analytics + * Usage: npm run cache:monitor + */ + +import { jiraAnalyticsCache } from '../src/services/integrations/jira/analytics-cache'; +import * as readline from 'readline'; + +function displayCacheStats() { + console.log('\n📊 === STATISTIQUES DU CACHE JIRA ANALYTICS ==='); + + const stats = jiraAnalyticsCache.getStats(); + + console.log(`\n📈 Total des entrĂ©es: ${stats.totalEntries}`); + + if (stats.projects.length === 0) { + console.log('📭 Aucune donnĂ©e en cache'); + return; + } + + console.log('\n📋 Projets en cache:'); + stats.projects.forEach(project => { + const status = project.isExpired ? '❌ EXPIRÉ' : '✅ VALIDE'; + console.log(` ‱ ${project.projectKey}:`); + console.log(` - Âge: ${project.age}`); + console.log(` - TTL: ${project.ttl}`); + console.log(` - Expire dans: ${project.expiresIn}`); + console.log(` - Taille: ${Math.round(project.size / 1024)}KB`); + console.log(` - Statut: ${status}`); + console.log(''); + }); +} + +function displayCacheActions() { + console.log('\n🔧 === ACTIONS DISPONIBLES ==='); + console.log('1. Afficher les statistiques'); + console.log('2. Forcer le nettoyage'); + console.log('3. Invalider tout le cache'); + console.log('4. Surveiller en temps rĂ©el (Ctrl+C pour arrĂȘter)'); + console.log('5. Quitter'); +} + +async function monitorRealtime() { + console.log('\n👀 Surveillance en temps rĂ©el (Ctrl+C pour arrĂȘter)...'); + + const interval = setInterval(() => { + console.clear(); + displayCacheStats(); + console.log('\n⏰ Mise Ă  jour toutes les 5 secondes...'); + }, 5000); + + // GĂ©rer l'arrĂȘt propre + process.on('SIGINT', () => { + clearInterval(interval); + console.log('\n\n👋 Surveillance arrĂȘtĂ©e'); + process.exit(0); + }); +} + +async function main() { + console.log('🚀 Cache Monitor Jira Analytics'); + + const args = process.argv.slice(2); + const command = args[0]; + + switch (command) { + case 'stats': + displayCacheStats(); + break; + + case 'cleanup': + console.log('\nđŸ§č Nettoyage forcĂ© du cache...'); + const cleaned = jiraAnalyticsCache.forceCleanup(); + console.log(`✅ ${cleaned} entrĂ©es supprimĂ©es`); + break; + + case 'clear': + console.log('\nđŸ—‘ïž Invalidation de tout le cache...'); + jiraAnalyticsCache.invalidateAll(); + console.log('✅ Cache vidĂ©'); + break; + + case 'monitor': + await monitorRealtime(); + break; + + default: + displayCacheStats(); + displayCacheActions(); + + // Interface interactive simple + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + const askAction = () => { + rl.question('\nChoisissez une action (1-5): ', async (answer: string) => { + switch (answer.trim()) { + case '1': + displayCacheStats(); + askAction(); + break; + case '2': + const cleaned = jiraAnalyticsCache.forceCleanup(); + console.log(`✅ ${cleaned} entrĂ©es supprimĂ©es`); + askAction(); + break; + case '3': + jiraAnalyticsCache.invalidateAll(); + console.log('✅ Cache vidĂ©'); + askAction(); + break; + case '4': + rl.close(); + await monitorRealtime(); + break; + case '5': + console.log('👋 Au revoir !'); + rl.close(); + process.exit(0); + break; + default: + console.log('❌ Action invalide'); + askAction(); + } + }); + }; + + askAction(); + } +} + +// ExĂ©cution du script +main().catch(console.error); diff --git a/src/services/integrations/jira/analytics-cache.ts b/src/services/integrations/jira/analytics-cache.ts index a1f670c..a706cc6 100644 --- a/src/services/integrations/jira/analytics-cache.ts +++ b/src/services/integrations/jira/analytics-cache.ts @@ -1,20 +1,24 @@ /** * Service de cache pour les analytics Jira - * Cache en mĂ©moire avec invalidation manuelle + * Cache en mĂ©moire avec invalidation manuelle et TTL */ import { JiraAnalytics } from '@/lib/types'; +import { createHash } from 'crypto'; interface CacheEntry { data: JiraAnalytics; timestamp: number; projectKey: string; configHash: string; // Hash de la config Jira pour dĂ©tecter les changements + ttl: number; // Time To Live en millisecondes } class JiraAnalyticsCacheService { private cache = new Map(); private readonly CACHE_KEY_PREFIX = 'jira-analytics:'; + private readonly DEFAULT_TTL = 30 * 60 * 1000; // 30 minutes par dĂ©faut + private cleanupInterval: NodeJS.Timeout | null = null; /** * GĂ©nĂšre une clĂ© de cache basĂ©e sur la config Jira @@ -24,22 +28,15 @@ class JiraAnalyticsCacheService { } /** - * GĂ©nĂšre un hash de la configuration Jira pour dĂ©tecter les changements + * GĂ©nĂšre un hash sĂ©curisĂ© de la configuration Jira pour dĂ©tecter les changements */ private generateConfigHash(config: { baseUrl: string; email: string; apiToken: string; projectKey: string }): string { const configString = `${config.baseUrl}|${config.email}|${config.apiToken}|${config.projectKey}`; - // Simple hash (pour production, utiliser crypto.createHash) - let hash = 0; - for (let i = 0; i < configString.length; i++) { - const char = configString.charCodeAt(i); - hash = ((hash << 5) - hash) + char; - hash = hash & hash; // Convert to 32-bit integer - } - return hash.toString(); + return createHash('sha256').update(configString).digest('hex').substring(0, 16); } /** - * RĂ©cupĂšre les analytics depuis le cache si disponible + * RĂ©cupĂšre les analytics depuis le cache si disponible et non expirĂ©es */ get(config: { baseUrl: string; email: string; apiToken: string; projectKey: string }): JiraAnalytics | null { const configHash = this.generateConfigHash(config); @@ -59,14 +56,24 @@ class JiraAnalyticsCacheService { return null; } - console.log(`✅ Cache HIT pour projet ${config.projectKey} (${this.getAgeDescription(entry.timestamp)})`); + // VĂ©rifier si l'entrĂ©e a expirĂ© + const now = Date.now(); + const isExpired = (now - entry.timestamp) > entry.ttl; + + if (isExpired) { + console.log(`⏰ Cache EXPIRÉ pour projet ${config.projectKey} (${this.getAgeDescription(entry.timestamp)})`); + this.cache.delete(cacheKey); + return null; + } + + console.log(`✅ Cache HIT pour projet ${config.projectKey} (${this.getAgeDescription(entry.timestamp)}, expire dans ${this.getTimeUntilExpiry(entry.timestamp, entry.ttl)})`); return entry.data; } /** - * Stocke les analytics dans le cache + * Stocke les analytics dans le cache avec TTL personnalisĂ© */ - set(config: { baseUrl: string; email: string; apiToken: string; projectKey: string }, data: JiraAnalytics): void { + set(config: { baseUrl: string; email: string; apiToken: string; projectKey: string }, data: JiraAnalytics, ttl?: number): void { const configHash = this.generateConfigHash(config); const cacheKey = this.getCacheKey(config.projectKey, configHash); @@ -74,11 +81,15 @@ class JiraAnalyticsCacheService { data, timestamp: Date.now(), projectKey: config.projectKey, - configHash + configHash, + ttl: ttl || this.DEFAULT_TTL }; this.cache.set(cacheKey, entry); - console.log(`đŸ’Ÿ Analytics mises en cache pour projet ${config.projectKey}`); + console.log(`đŸ’Ÿ Analytics mises en cache pour projet ${config.projectKey} (TTL: ${this.formatTTL(entry.ttl)})`); + + // DĂ©marrer le nettoyage automatique si pas dĂ©jĂ  fait + this.startCleanupInterval(); } /** @@ -106,16 +117,27 @@ class JiraAnalyticsCacheService { } /** - * Retourne les statistiques du cache + * Retourne les statistiques du cache avec informations TTL */ getStats(): { totalEntries: number; - projects: Array<{ projectKey: string; age: string; size: number }>; + projects: Array<{ + projectKey: string; + age: string; + size: number; + ttl: string; + expiresIn: string; + isExpired: boolean; + }>; } { + const now = Date.now(); const projects = Array.from(this.cache.entries()).map(([, entry]) => ({ projectKey: entry.projectKey, age: this.getAgeDescription(entry.timestamp), - size: JSON.stringify(entry.data).length + size: JSON.stringify(entry.data).length, + ttl: this.formatTTL(entry.ttl), + expiresIn: this.getTimeUntilExpiry(entry.timestamp, entry.ttl), + isExpired: (now - entry.timestamp) > entry.ttl })); return { @@ -141,6 +163,72 @@ class JiraAnalyticsCacheService { } } + /** + * Calcule le temps restant avant expiration + */ + private getTimeUntilExpiry(timestamp: number, ttl: number): string { + const now = Date.now(); + const remainingMs = ttl - (now - timestamp); + + if (remainingMs <= 0) return 'expirĂ©'; + + const remainingMinutes = Math.floor(remainingMs / (1000 * 60)); + const remainingHours = Math.floor(remainingMinutes / 60); + + if (remainingHours > 0) { + return `${remainingHours}h${remainingMinutes % 60}m`; + } else { + return `${remainingMinutes}m`; + } + } + + /** + * Formate un TTL en texte lisible + */ + private formatTTL(ttl: number): string { + const minutes = Math.floor(ttl / (1000 * 60)); + const hours = Math.floor(minutes / 60); + + if (hours > 0) { + return `${hours}h${minutes % 60}m`; + } else { + return `${minutes}m`; + } + } + + /** + * DĂ©marre l'intervalle de nettoyage automatique + */ + private startCleanupInterval(): void { + if (this.cleanupInterval) return; // DĂ©jĂ  dĂ©marrĂ© + + // Nettoyer toutes les 5 minutes + this.cleanupInterval = setInterval(() => { + this.cleanupExpiredEntries(); + }, 5 * 60 * 1000); + + console.log('đŸ§č Nettoyage automatique du cache dĂ©marrĂ© (toutes les 5 minutes)'); + } + + /** + * Nettoie les entrĂ©es expirĂ©es du cache + */ + private cleanupExpiredEntries(): void { + const now = Date.now(); + let cleanedCount = 0; + + for (const [key, entry] of this.cache.entries()) { + if ((now - entry.timestamp) > entry.ttl) { + this.cache.delete(key); + cleanedCount++; + } + } + + if (cleanedCount > 0) { + console.log(`đŸ§č Nettoyage automatique: ${cleanedCount} entrĂ©es expirĂ©es supprimĂ©es`); + } + } + /** * VĂ©rifie si une entrĂ©e existe pour un projet */ @@ -149,6 +237,30 @@ class JiraAnalyticsCacheService { const cacheKey = this.getCacheKey(config.projectKey, configHash); return this.cache.has(cacheKey); } + + /** + * ArrĂȘte le nettoyage automatique + */ + stopCleanupInterval(): void { + if (this.cleanupInterval) { + clearInterval(this.cleanupInterval); + this.cleanupInterval = null; + console.log('🛑 Nettoyage automatique du cache arrĂȘtĂ©'); + } + } + + /** + * Force un nettoyage immĂ©diat des entrĂ©es expirĂ©es + */ + forceCleanup(): number { + const beforeCount = this.cache.size; + this.cleanupExpiredEntries(); + const afterCount = this.cache.size; + const cleanedCount = beforeCount - afterCount; + + console.log(`đŸ§č Nettoyage forcĂ©: ${cleanedCount} entrĂ©es supprimĂ©es`); + return cleanedCount; + } } // Instance singleton