// ======================================== // FICHIER: ModeManager.js // RESPONSABILITÉ: Gestionnaire modes exclusifs serveur // MODES: MANUAL (interface client) | AUTO (traitement batch GSheets) // ======================================== const { logSh } = require('../ErrorReporting'); const fs = require('fs'); const path = require('path'); /** * GESTIONNAIRE MODES EXCLUSIFS * Gère le basculement entre mode MANUAL et AUTO de façon exclusive */ class ModeManager { // ======================================== // CONSTANTES ET ÉTAT // ======================================== static MODES = { MANUAL: 'manual', // Interface client + API + WebSocket AUTO: 'auto' // Traitement batch Google Sheets }; static currentMode = null; static isLocked = false; static lockReason = null; static modeStartTime = null; static activeServices = { manualServer: null, autoProcessor: null, websocketServer: null }; // Stats par mode static stats = { manual: { sessions: 0, requests: 0, lastActivity: null }, auto: { processed: 0, errors: 0, lastProcessing: null } }; // ======================================== // INITIALISATION ET DÉTECTION MODE // ======================================== /** * Initialise le gestionnaire de modes * @param {string} initialMode - Mode initial (manual|auto|detect) */ static async initialize(initialMode = 'detect') { logSh('🎛️ Initialisation ModeManager...', 'INFO'); try { // Détecter mode selon arguments ou config const detectedMode = this.detectIntendedMode(initialMode); logSh(`🎯 Mode détecté: ${detectedMode.toUpperCase()}`, 'INFO'); // Nettoyer état précédent si nécessaire await this.cleanupPreviousState(); // Basculer vers le mode détecté await this.switchToMode(detectedMode); // Sauvegarder état this.saveModeState(); logSh(`✅ ModeManager initialisé en mode ${this.currentMode.toUpperCase()}`, 'INFO'); return this.currentMode; } catch (error) { logSh(`❌ Erreur initialisation ModeManager: ${error.message}`, 'ERROR'); throw new Error(`Échec initialisation ModeManager: ${error.message}`); } } /** * Détecte le mode souhaité selon arguments CLI et env */ static detectIntendedMode(initialMode) { // 1. Argument explicite if (initialMode === this.MODES.MANUAL || initialMode === this.MODES.AUTO) { return initialMode; } // 2. Arguments de ligne de commande const args = process.argv.slice(2); const modeArg = args.find(arg => arg.startsWith('--mode=')); if (modeArg) { const mode = modeArg.split('=')[1]; if (Object.values(this.MODES).includes(mode)) { return mode; } } // 3. Variable d'environnement const envMode = process.env.SERVER_MODE?.toLowerCase(); if (Object.values(this.MODES).includes(envMode)) { return envMode; } // 4. Script npm spécifique const npmScript = process.env.npm_lifecycle_event; if (npmScript === 'auto') return this.MODES.AUTO; // 5. Défaut = MANUAL return this.MODES.MANUAL; } // ======================================== // CHANGEMENT DE MODES // ======================================== /** * Bascule vers un mode spécifique */ static async switchToMode(targetMode, force = false) { if (!Object.values(this.MODES).includes(targetMode)) { throw new Error(`Mode invalide: ${targetMode}`); } if (this.currentMode === targetMode) { logSh(`Mode ${targetMode} déjà actif`, 'DEBUG'); return true; } // Vérifier si changement possible if (!force && !await this.canSwitchToMode(targetMode)) { throw new Error(`Impossible de basculer vers ${targetMode}: ${this.lockReason}`); } logSh(`🔄 Basculement ${this.currentMode || 'NONE'} → ${targetMode}...`, 'INFO'); try { // Arrêter mode actuel await this.stopCurrentMode(); // Démarrer nouveau mode await this.startMode(targetMode); // Mettre à jour état this.currentMode = targetMode; this.modeStartTime = Date.now(); this.lockReason = null; logSh(`✅ Basculement terminé: Mode ${targetMode.toUpperCase()} actif`, 'INFO'); return true; } catch (error) { logSh(`❌ Échec basculement vers ${targetMode}: ${error.message}`, 'ERROR'); // Tentative de récupération try { await this.emergencyRecovery(); } catch (recoveryError) { logSh(`❌ Échec récupération d'urgence: ${recoveryError.message}`, 'ERROR'); } throw error; } } /** * Vérifie si le basculement est possible */ static async canSwitchToMode(targetMode) { // Mode verrouillé if (this.isLocked) { this.lockReason = 'Mode verrouillé pour opération critique'; return false; } // Vérifications spécifiques par mode switch (targetMode) { case this.MODES.MANUAL: return await this.canSwitchToManual(); case this.MODES.AUTO: return await this.canSwitchToAuto(); default: return false; } } /** * Peut-on basculer vers MANUAL ? */ static async canSwitchToManual() { // Si mode AUTO actif, vérifier processus if (this.currentMode === this.MODES.AUTO) { const autoProcessor = this.activeServices.autoProcessor; if (autoProcessor && autoProcessor.isProcessing()) { this.lockReason = 'Traitement automatique en cours, arrêt requis'; return false; } } return true; } /** * Peut-on basculer vers AUTO ? */ static async canSwitchToAuto() { // Si mode MANUAL actif, vérifier clients if (this.currentMode === this.MODES.MANUAL) { const manualServer = this.activeServices.manualServer; if (manualServer && manualServer.hasActiveClients()) { this.lockReason = 'Clients actifs en mode MANUAL, déconnexion requise'; return false; } } return true; } // ======================================== // DÉMARRAGE ET ARRÊT SERVICES // ======================================== /** * Démarre un mode spécifique */ static async startMode(mode) { logSh(`🚀 Démarrage mode ${mode.toUpperCase()}...`, 'DEBUG'); switch (mode) { case this.MODES.MANUAL: await this.startManualMode(); break; case this.MODES.AUTO: await this.startAutoMode(); break; default: throw new Error(`Mode de démarrage inconnu: ${mode}`); } } /** * Démarre le mode MANUAL */ static async startManualMode() { const { ManualServer } = require('./ManualServer'); logSh('🎯 Démarrage ManualServer...', 'DEBUG'); this.activeServices.manualServer = new ManualServer(); await this.activeServices.manualServer.start(); logSh('✅ Mode MANUAL démarré', 'DEBUG'); } /** * Démarre le mode AUTO */ static async startAutoMode() { const { AutoProcessor } = require('./AutoProcessor'); logSh('🤖 Démarrage AutoProcessor...', 'DEBUG'); this.activeServices.autoProcessor = new AutoProcessor(); await this.activeServices.autoProcessor.start(); logSh('✅ Mode AUTO démarré', 'DEBUG'); } /** * Arrête le mode actuel */ static async stopCurrentMode() { if (!this.currentMode) return; logSh(`🛑 Arrêt mode ${this.currentMode.toUpperCase()}...`, 'DEBUG'); try { switch (this.currentMode) { case this.MODES.MANUAL: await this.stopManualMode(); break; case this.MODES.AUTO: await this.stopAutoMode(); break; } logSh(`✅ Mode ${this.currentMode.toUpperCase()} arrêté`, 'DEBUG'); } catch (error) { logSh(`⚠️ Erreur arrêt mode ${this.currentMode}: ${error.message}`, 'WARNING'); // Continue malgré l'erreur pour permettre le changement } } /** * Arrête le mode MANUAL */ static async stopManualMode() { if (this.activeServices.manualServer) { await this.activeServices.manualServer.stop(); this.activeServices.manualServer = null; } } /** * Arrête le mode AUTO */ static async stopAutoMode() { if (this.activeServices.autoProcessor) { await this.activeServices.autoProcessor.stop(); this.activeServices.autoProcessor = null; } } // ======================================== // ÉTAT ET MONITORING // ======================================== /** * État actuel du gestionnaire */ static getStatus() { return { currentMode: this.currentMode, isLocked: this.isLocked, lockReason: this.lockReason, modeStartTime: this.modeStartTime, uptime: this.modeStartTime ? Date.now() - this.modeStartTime : 0, stats: { ...this.stats }, activeServices: { manualServer: !!this.activeServices.manualServer, autoProcessor: !!this.activeServices.autoProcessor } }; } /** * Vérifie si mode MANUAL actif */ static isManualMode() { return this.currentMode === this.MODES.MANUAL; } /** * Vérifie si mode AUTO actif */ static isAutoMode() { return this.currentMode === this.MODES.AUTO; } /** * Verrouille le mode actuel */ static lockMode(reason = 'Opération critique') { this.isLocked = true; this.lockReason = reason; logSh(`🔒 Mode ${this.currentMode} verrouillé: ${reason}`, 'INFO'); } /** * Déverrouille le mode */ static unlockMode() { this.isLocked = false; this.lockReason = null; logSh(`🔓 Mode ${this.currentMode} déverrouillé`, 'INFO'); } // ======================================== // GESTION ERREURS ET RÉCUPÉRATION // ======================================== /** * Nettoyage état précédent */ static async cleanupPreviousState() { logSh('🧹 Nettoyage état précédent...', 'DEBUG'); // Arrêter tous les services actifs await this.stopCurrentMode(); // Reset état this.isLocked = false; this.lockReason = null; logSh('✅ Nettoyage terminé', 'DEBUG'); } /** * Récupération d'urgence */ static async emergencyRecovery() { logSh('🚨 Récupération d\'urgence...', 'WARNING'); try { // Forcer arrêt de tous les services await this.forceStopAllServices(); // Reset état complet this.currentMode = null; this.isLocked = false; this.lockReason = null; this.modeStartTime = null; logSh('✅ Récupération d\'urgence terminée', 'INFO'); } catch (error) { logSh(`❌ Échec récupération d'urgence: ${error.message}`, 'ERROR'); throw error; } } /** * Arrêt forcé de tous les services */ static async forceStopAllServices() { const services = Object.keys(this.activeServices); for (const serviceKey of services) { const service = this.activeServices[serviceKey]; if (service) { try { if (typeof service.stop === 'function') { await service.stop(); } } catch (error) { logSh(`⚠️ Erreur arrêt forcé ${serviceKey}: ${error.message}`, 'WARNING'); } this.activeServices[serviceKey] = null; } } } // ======================================== // PERSISTANCE ET CONFIGURATION // ======================================== /** * Sauvegarde l'état du mode */ static saveModeState() { try { const stateFile = path.join(__dirname, '../..', 'mode-state.json'); const state = { currentMode: this.currentMode, modeStartTime: this.modeStartTime, stats: this.stats, timestamp: new Date().toISOString() }; fs.writeFileSync(stateFile, JSON.stringify(state, null, 2)); } catch (error) { logSh(`⚠️ Erreur sauvegarde état mode: ${error.message}`, 'WARNING'); } } /** * Restaure l'état du mode */ static loadModeState() { try { const stateFile = path.join(__dirname, '../..', 'mode-state.json'); if (fs.existsSync(stateFile)) { const state = JSON.parse(fs.readFileSync(stateFile, 'utf8')); this.stats = state.stats || this.stats; return state; } } catch (error) { logSh(`⚠️ Erreur chargement état mode: ${error.message}`, 'WARNING'); } return null; } } // ============= EXPORTS ============= module.exports = { ModeManager };