From 77a310976b449e0df548dda597086ee11f7a5ed2 Mon Sep 17 00:00:00 2001 From: Pierre Marx Date: Wed, 18 Mar 2026 17:31:30 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20migration=20Socket.IO=20natif=20?= =?UTF-8?q?=E2=80=94=20login,=20terminaux=20REST,=20health=20check=20(clos?= =?UTF-8?q?es=20#3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remplace toute la couche SignalR par une connexion Socket.IO directe au serveur Python (port 8004). Auth au handshake, reconnexion native illimitée, terminaux via REST GET /terminals. - socketio-adapter.js : connect/logoff/disconnect, events login_ok/login_error - main.js : initializeSocketIO, health check net.request, terminaux REST - renderer.js : IPC signalr-status → server-status - config.json : clé socketio (plus signalR) - Version 2.0.0 --- config.json | 10 +- main.js | 650 ++++++++++++++++---------------------------- package.json | 2 +- renderer.js | 48 ++-- socketio-adapter.js | 151 ++++++++++ 5 files changed, 414 insertions(+), 447 deletions(-) create mode 100644 socketio-adapter.js diff --git a/config.json b/config.json index 943971d..a0ac8aa 100644 --- a/config.json +++ b/config.json @@ -1,8 +1,6 @@ { - "signalR": { - "enabled": true, - "serverUrl": "http://10.90.20.201:8002/signalR", - "serviceProvider": "RDVPREM", - "terminalsSimulation": ["3001", "3002", "3003", "3004", "3005"] + "socketio": { + "serverUrl": "http://10.90.20.201:8004", + "serviceProvider": "RDVPREM" } -} \ No newline at end of file +} diff --git a/main.js b/main.js index 798a6b9..0ef9556 100644 --- a/main.js +++ b/main.js @@ -1,51 +1,46 @@ -const { app, BrowserWindow, ipcMain, session } = require('electron'); +const { app, BrowserWindow, ipcMain, session, net } = require('electron'); const path = require('path'); const fs = require('fs'); const os = require('os'); -const signalR = require('@microsoft/signalr'); -const ConnectionManager = require('./connection-manager'); +const SocketIOAdapter = require('./socketio-adapter'); let mainWindow; let config; let currentAgent = null; -let currentTerminal = null; // Terminal téléphonique de l'agent connecté -let signalRConnection = null; -let signalRStatus = 'disconnected'; // disconnected, connecting, connected, error -let agentConnectionInfo = null; // Informations complètes retournées par SignalR +let currentTerminal = null; +let adapter = null; +let serverStatus = 'disconnected'; // disconnected, connecting, connected, error +let agentConnectionInfo = null; +let healthCheckInterval = null; -// Configuration du système de logs SignalR -const SIGNALR_LOG_DIR = path.join(os.homedir(), '.simpleconnect-ng'); -const SIGNALR_LOG_FILE = path.join(SIGNALR_LOG_DIR, 'signalr.log'); +// Configuration du systeme de logs +const LOG_DIR = path.join(os.homedir(), '.simpleconnect-ng'); +const LOG_FILE = path.join(LOG_DIR, 'socketio.log'); -// Créer le répertoire de logs s'il n'existe pas function ensureLogDirectory() { - if (!fs.existsSync(SIGNALR_LOG_DIR)) { - fs.mkdirSync(SIGNALR_LOG_DIR, { recursive: true }); - console.log(`📁 Répertoire de logs créé: ${SIGNALR_LOG_DIR}`); + if (!fs.existsSync(LOG_DIR)) { + fs.mkdirSync(LOG_DIR, { recursive: true }); } } -// Fonction pour écrire dans le fichier de log SignalR -function logToSignalRFile(message, data = null) { +function logToFile(message, data = null) { ensureLogDirectory(); - + const timestamp = new Date().toISOString(); let logEntry = `[${timestamp}] ${message}`; - + if (data) { logEntry += '\n' + JSON.stringify(data, null, 2); } - - logEntry += '\n' + '─'.repeat(80) + '\n'; - - // Ajouter au fichier (append) - fs.appendFileSync(SIGNALR_LOG_FILE, logEntry, 'utf8'); + + logEntry += '\n' + '-'.repeat(80) + '\n'; + + fs.appendFileSync(LOG_FILE, logEntry, 'utf8'); } -// Logger dans la console ET dans le fichier -function logSignalR(message, data = null) { +function log(message, data = null) { console.log(message, data || ''); - logToSignalRFile(message, data); + logToFile(message, data); } // Charger la configuration @@ -55,7 +50,7 @@ function loadConfig() { config = JSON.parse(configData); } -// Créer la fenêtre principale +// Creer la fenetre principale function createWindow() { mainWindow = new BrowserWindow({ width: 1400, @@ -68,211 +63,92 @@ function createWindow() { }, icon: path.join(__dirname, 'icon.png'), title: `SimpleConnect v${app.getVersion()}`, - autoHideMenuBar: true // Cache la barre de menu par défaut + autoHideMenuBar: true }); - // Supprimer complètement le menu (pour Linux/Windows) mainWindow.setMenuBarVisibility(false); - - // Charger l'interface HTML mainWindow.loadFile('index.html'); - // Forcer le titre après le chargement de la page (le HTML l'écrase sinon) mainWindow.webContents.on('did-finish-load', () => { mainWindow.setTitle(`SimpleConnect v${app.getVersion()}`); }); - // Ouvrir les DevTools uniquement en mode développement if (process.env.NODE_ENV === 'development') { mainWindow.webContents.openDevTools(); } - // Gérer la fermeture de la fenêtre mainWindow.on('closed', () => { mainWindow = null; }); } -// === GESTION SIGNALR/WEBSOCKET === -function initializeSignalR() { - if (!config.signalR || !config.signalR.enabled) { - console.log('SignalR/WebSocket désactivé dans la configuration'); - signalRStatus = 'disabled'; - sendSignalRStatus(); +// === GESTION SOCKET.IO === + +function initializeSocketIO() { + if (!config.socketio || !config.socketio.serverUrl) { + console.log('Socket.IO non configure'); + serverStatus = 'disabled'; + sendServerStatus(); return; } - try { - // Utiliser le ConnectionManager avec fallback automatique SignalR → WebSocket - const connectionManager = new ConnectionManager(config.ServerIp || config.signalR.serverUrl.replace('http://', '').replace('/signalR', '')); - - // La connexion sera établie plus tard avec fallback automatique - signalRConnection = connectionManager; + adapter = new SocketIOAdapter(config.socketio.serverUrl); - // Les handlers d'état seront configurés après la connexion - - // Démarrer la connexion (les handlers seront configurés après) - startSignalRConnection(); - - } catch (error) { - console.error('Erreur initialisation SignalR:', error); - signalRStatus = 'error'; - sendSignalRStatus(); - } + // Demarrer le health check polling + startHealthCheck(); } -function setupSignalRHandlers() { - // Configuration des handlers après la connexion - const connection = signalRConnection.getConnection(); - if (!connection) { - console.error('Pas de connexion active pour configurer les handlers'); - return; - } +// Health check periodique via GET /health +function startHealthCheck() { + const checkHealth = () => { + const serverUrl = config.socketio.serverUrl; - // === LOGGER UNIVERSEL POUR TOUS LES MESSAGES SIGNALR === - // Intercepter TOUS les messages reçus du serveur pour découvrir les événements disponibles - - // Initialiser le fichier de log avec une session - logSignalR('════════════════════════════════════════════════════════════'); - logSignalR('🚀 NOUVELLE SESSION SIGNALR DÉMARRÉE'); - logSignalR(`Application: SimpleConnect v${app.getVersion()}`); - logSignalR(`Serveur SignalR: ${config.signalR.serverUrl}`); - logSignalR(`Service Provider: ${config.signalR.serviceProvider}`); - logSignalR('════════════════════════════════════════════════════════════'); - - // Liste des événements connus pour les logger différemment - const knownEvents = ['IpbxEvent']; - - // Créer un proxy pour intercepter tous les appels .on() - const originalOn = signalRConnection.on.bind(signalRConnection); - - // Logger tous les événements possibles en essayant d'écouter les plus communs - const possibleEvents = [ - 'IpbxEvent', // Événements téléphoniques (confirmé) - 'AgentStatusChanged', // Changement de statut agent - 'QueueUpdate', // Mise à jour des files - 'CallReceived', // Appel entrant - 'CallEnded', // Fin d'appel - 'MessageReceived', // Messages - 'Notification', // Notifications générales - 'StatusUpdate', // Mises à jour de statut - 'SystemMessage', // Messages système - 'BroadcastMessage', // Messages broadcast - 'AgentUpdate', // Mises à jour agent - 'QueueStatistics', // Statistiques de file - 'PresenceUpdate' // Mise à jour de présence - ]; - - // Écouter tous les événements possibles et logger ce qu'on reçoit - possibleEvents.forEach(eventName => { - connection.on(eventName, (...args) => { - // Logger dans la console avec formatage - console.log('═══════════════════════════════════════════════════════════'); - console.log(`📨 MESSAGE SIGNALR REÇU: ${eventName}`); - console.log('Timestamp:', new Date().toISOString()); - console.log('Nombre d\'arguments:', args.length); - - // Logger chaque argument en détail - args.forEach((arg, index) => { - console.log(`Argument ${index}:`, JSON.stringify(arg, null, 2)); - }); - - console.log('═══════════════════════════════════════════════════════════'); - - // Logger dans le fichier avec structure - const logData = { - event: eventName, - timestamp: new Date().toISOString(), - argumentCount: args.length, - arguments: args.map((arg, index) => ({ - index: index, - type: typeof arg, - value: arg - })), - agent: currentAgent ? { - id: currentAgent.id, - name: currentAgent.name, - terminal: currentTerminal - } : null - }; - - logSignalR(`📨 MESSAGE SIGNALR REÇU: ${eventName}`, logData); - - // Si c'est IpbxEvent, traiter comme avant - if (eventName === 'IpbxEvent') { - handleIpbxEventOriginal(args); + const request = net.request(`${serverUrl}/health`); + + request.on('response', (response) => { + if (response.statusCode === 200) { + if (serverStatus !== 'connected') { + // Ne passer en 'connected' que si on n'a pas d'agent connecte + // (sinon le status est deja 'connected' via le login) + if (!currentAgent) { + serverStatus = 'connected'; + sendServerStatus(); + } + } + } else { + serverStatus = 'error'; + sendServerStatus(); } }); - }); - - // Fonction originale pour traiter les IpbxEvent - function handleIpbxEventOriginal(args) { - if (!args || !agentConnectionInfo) return; - - const [name, eventArgs] = args; - const event = eventArgs?.[0] || args[0]; - - console.log('🔍 Traitement IpbxEvent:', { - eventCode: event.eventCode, - terminal: event.terminal, - queueName: event.queueName, - fullEvent: event // Logger l'objet complet pour voir tous les champs + + request.on('error', () => { + serverStatus = 'error'; + sendServerStatus(); }); - - // Vérifier que l'événement est pour notre terminal - if (event.terminal !== currentTerminal) { - console.log('⚠️ Événement ignoré - Terminal différent:', event.terminal, '!==', currentTerminal); - return; - } - - // Gérer les différents types d'événements - switch(event.eventCode) { - case 0: - console.log('📞 Code 0: Appel entrant/sonnerie (non implémenté)'); - break; - case 1: // Appel décroché - console.log('✅ Code 1: Appel décroché - Traitement...'); - handleCallPickedUp(event); - break; - case 2: // Appel raccroché - console.log('📴 Code 2: Appel raccroché - Traitement...'); - handleCallHungUp(event); - break; - case 3: - console.log('⏸️ Code 3: Mise en attente (non implémenté)'); - break; - case 4: - console.log('↔️ Code 4: Transfert d\'appel (non implémenté)'); - break; - case 5: - console.log('👥 Code 5: Conférence (non implémenté)'); - break; - default: - console.log('❓ Code événement inconnu:', event.eventCode); - console.log('Données complètes:', JSON.stringify(event, null, 2)); - } - } - - // Logger aussi les méthodes qu'on peut invoquer - console.log('📋 MÉTHODES SIGNALR DISPONIBLES POUR INVOCATION:'); - console.log('- AgentLogin(email, password, terminal)'); - console.log('- AgentLogoff(accessCode)'); - console.log('- GetTerminalListByServiceProvider(serviceProvider)'); - console.log('ℹ️ D\'autres méthodes peuvent être disponibles sur le serveur'); + + request.end(); + }; + + // Check immediat puis toutes les 10s + checkHealth(); + healthCheckInterval = setInterval(checkHealth, 10000); } -// Gérer un appel entrant +function sendServerStatus() { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send('server-status', serverStatus); + } +} + +// Gerer un appel entrant function handleCallPickedUp(event) { if (!mainWindow || !agentConnectionInfo) return; - - // Identifier le centre correspondant à la file + const centres = processApplicationUrls(agentConnectionInfo.connList); const centre = centres.find(c => c.queueName === event.queueName); - + if (centre) { console.log('Basculement vers le centre:', centre.nom); - - // Envoyer l'instruction de basculement à la fenêtre mainWindow.webContents.send('switch-to-center', { centreId: centre.id, centreName: centre.nom, @@ -281,17 +157,15 @@ function handleCallPickedUp(event) { eventType: 'call_pickup' }); } else { - console.warn('Aucun centre trouvé pour la file:', event.queueName); + console.warn('Aucun centre trouve pour la file:', event.queueName); } } -// Gérer la fin d'un appel +// Gerer la fin d'un appel function handleCallHungUp(event) { if (!mainWindow) return; - + console.log('Fin d\'appel sur la file:', event.queueName); - - // Envoyer l'instruction de libération à la fenêtre mainWindow.webContents.send('release-center', { queueName: event.queueName, terminal: event.terminal, @@ -299,68 +173,45 @@ function handleCallHungUp(event) { }); } -async function startSignalRConnection() { - try { - signalRStatus = 'connecting'; - sendSignalRStatus(); - logSignalR('🔌 Tentative de connexion au serveur...', { - serverUrl: config.ServerIp || config.signalR.serverUrl, - status: 'connecting' - }); - - // Le ConnectionManager gère le fallback automatiquement - const connection = await signalRConnection.connect(); - - // Déterminer quel type de connexion a réussi - const connectionInfo = signalRConnection.getConnectionInfo(); - - console.log(`Connexion établie via ${connectionInfo.type}`); - logSignalR(`✅ Connexion établie avec succès (${connectionInfo.type})`, { - connectionType: connectionInfo.type, - isSignalR: connectionInfo.isSignalR, - isRestSocketIO: connectionInfo.isRestSocketIO, - status: 'connected', - serverUrl: connectionInfo.serverUrl - }); - - // Maintenant configurer les handlers sur la connexion active - setupSignalRHandlers(); - - signalRStatus = 'connected'; - sendSignalRStatus(); - - } catch (error) { - console.error('Erreur connexion SignalR:', error); - logSignalR('❌ Erreur de connexion SignalR', { - error: error.message, - stack: error.stack, - serverUrl: config.signalR.serverUrl - }); - signalRStatus = 'error'; - sendSignalRStatus(); - - // Réessayer dans 5 secondes - setTimeout(() => { - if (signalRConnection && signalRStatus !== 'connected') { - startSignalRConnection(); - } - }, 5000); - } -} +// Configurer les handlers d'evenements Socket.IO apres connexion +function setupEventHandlers() { + if (!adapter) return; -function sendSignalRStatus() { - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('signalr-status', signalRStatus); - } + log('Session Socket.IO demarree', { + serverUrl: config.socketio.serverUrl, + serviceProvider: config.socketio.serviceProvider + }); + + // Ecouter les evenements d'appels IPBX + adapter.on('ipbx_event', (data) => { + log('ipbx_event recu', data); + + if (!agentConnectionInfo) return; + + // Verifier que l'evenement est pour notre terminal + if (data.terminal !== currentTerminal) { + console.log('Evenement ignore - terminal different:', data.terminal, '!==', currentTerminal); + return; + } + + switch (data.eventCode) { + case 1: + handleCallPickedUp(data); + break; + case 2: + handleCallHungUp(data); + break; + default: + log('Code evenement non gere:', data); + } + }); } // Initialisation de l'application app.whenReady().then(() => { - // Supprimer le menu de l'application complètement (toutes plateformes) const { Menu } = require('electron'); Menu.setApplicationMenu(null); - - // Configuration de la session pour éviter les problèmes CORS + session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => { details.requestHeaders['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'; callback({ requestHeaders: details.requestHeaders }); @@ -377,33 +228,30 @@ app.whenReady().then(() => { loadConfig(); createWindow(); - - // Initialiser SignalR après le chargement de la config - initializeSignalR(); + initializeSocketIO(); }); -// Quitter quand toutes les fenêtres sont fermées +// Quitter quand toutes les fenetres sont fermees app.on('window-all-closed', async () => { - // Déconnexion propre avant de quitter - if (currentAgent && signalRConnection && signalRStatus === 'connected') { + if (healthCheckInterval) { + clearInterval(healthCheckInterval); + } + + // Deconnexion propre avant de quitter + if (currentAgent && adapter) { try { - await signalRConnection.invoke('AgentLogoff', currentAgent.accessCode); - console.log('Déconnexion agent avant fermeture'); + await adapter.logoff(); + console.log('Agent deconnecte avant fermeture'); } catch (error) { - console.error('Erreur déconnexion:', error); + console.error('Erreur deconnexion:', error); } } - - // Ne pas appeler disconnect() pour éviter l'envoi du CloseMessage - // Le serveur .NET ne supporte pas ce message - // On laisse la connexion se fermer naturellement avec l'application - + if (process.platform !== 'darwin') { app.quit(); } }); -// Réactiver l'app sur macOS app.on('activate', () => { if (mainWindow === null) { createWindow(); @@ -412,108 +260,107 @@ app.on('activate', () => { // === IPC HANDLERS === -// Obtenir la configuration ipcMain.handle('get-config', () => { return config; }); -// Obtenir la version de l'application ipcMain.handle('get-app-version', () => { return app.getVersion(); }); -// Obtenir le statut SignalR -ipcMain.handle('get-signalr-status', () => { - return signalRStatus; +// Renomme : get-signalr-status -> get-server-status +ipcMain.handle('get-server-status', () => { + return serverStatus; }); -// Récupérer la liste des terminaux téléphoniques +// Recuperer la liste des terminaux via REST ipcMain.handle('get-terminal-list', async () => { - // Mode simulation si SignalR non connecté - if (!signalRConnection || signalRStatus !== 'connected') { - console.log('SignalR non connecté, utilisation des terminaux de simulation'); - return config.signalR.terminalsSimulation || ['3001', '3002', '3003']; + if (!config.socketio || !config.socketio.serverUrl) { + return []; } - + try { - console.log('Récupération des terminaux pour:', config.signalR.serviceProvider); - logSignalR('📡 Invocation SignalR: GetTerminalListByServiceProvider', { - method: 'GetTerminalListByServiceProvider', - serviceProvider: config.signalR.serviceProvider + const provider = config.socketio.serviceProvider; + const url = `${config.socketio.serverUrl}/terminals?provider=${encodeURIComponent(provider)}`; + console.log('Recuperation des terminaux:', url); + + return new Promise((resolve, reject) => { + const request = net.request(url); + let body = ''; + + request.on('response', (response) => { + response.on('data', (chunk) => { + body += chunk.toString(); + }); + + response.on('end', () => { + if (response.statusCode === 200) { + try { + const terminals = JSON.parse(body); + log('Terminaux recuperes', { count: terminals.length, terminals }); + resolve(Array.isArray(terminals) ? terminals : []); + } catch (e) { + console.error('Erreur parsing terminaux:', e); + resolve([]); + } + } else { + console.error('Erreur terminaux HTTP', response.statusCode); + resolve([]); + } + }); + }); + + request.on('error', (error) => { + console.error('Erreur recuperation terminaux:', error); + resolve([]); + }); + + request.end(); }); - - const terminals = await signalRConnection.invoke( - 'GetTerminalListByServiceProvider', - config.signalR.serviceProvider - ); - - console.log('Terminaux disponibles:', terminals); - logSignalR('📞 Terminaux récupérés', { - count: terminals.length, - terminals: terminals - }); - return terminals || []; } catch (error) { - console.error('Erreur récupération terminaux:', error); - // Retourner les terminaux de simulation en cas d'erreur - return config.signalR.terminalsSimulation || ['3001', '3002', '3003']; + console.error('Erreur recuperation terminaux:', error); + return []; } }); -// Connexion agent via SignalR +// Connexion agent via Socket.IO ipcMain.handle('login-agent', async (event, credentials) => { - // Vérifier que SignalR est connecté - if (!signalRConnection || signalRStatus !== 'connected') { - return { - success: false, - message: 'Connexion au serveur SignalR non établie. Veuillez réessayer.' + if (!adapter) { + return { + success: false, + message: 'Socket.IO non initialise. Veuillez reessayer.' }; } try { console.log('Tentative de connexion agent:', credentials.email, 'Terminal:', credentials.terminal); - logSignalR('🔐 Tentative de connexion agent', { + log('Tentative de connexion agent', { email: credentials.email, terminal: credentials.terminal, forceDisconnect: credentials.forceDisconnect || false }); - - // Si déconnexion forcée demandée, déconnecter d'abord la session précédente - if (credentials.forceDisconnect) { - console.log('Déconnexion forcée demandée pour:', credentials.email); - try { - // Tenter la déconnexion avec le code d'accès - await signalRConnection.invoke('AgentLogoff', credentials.email); - console.log('Session précédente déconnectée avec succès'); - logSignalR('🔓 Session précédente déconnectée (forceDisconnect)', { - email: credentials.email - }); - } catch (logoffError) { - console.warn('Erreur lors de la déconnexion forcée (session peut-être déjà fermée):', logoffError.message); - logSignalR('⚠️ Erreur déconnexion forcée', { - email: credentials.email, - error: logoffError.message - }); - // Continuer même si la déconnexion échoue - la session est peut-être déjà fermée - } + + // Deconnecter l'adapter precedent s'il y en a un + if (adapter.state === 'connected') { + adapter.disconnect(); } - - // Appel SignalR pour l'authentification - logSignalR('📡 Invocation SignalR: AgentLogin', { - method: 'AgentLogin', - email: credentials.email, - terminal: credentials.terminal - }); - - const result = await signalRConnection.invoke('AgentLogin', + + // Recreer l'adapter pour une connexion fraiche + adapter = new SocketIOAdapter(config.socketio.serverUrl); + + // Configurer les handlers d'evenements AVANT connect + setupEventHandlers(); + + // Connexion avec auth au handshake + const result = await adapter.connect( credentials.email, credentials.password, credentials.terminal ); - + if (result) { - console.log('Connexion réussie:', result); - logSignalR('✅ Connexion agent réussie', { + console.log('Connexion reussie:', result); + log('Connexion agent reussie', { accessCode: result.accessCode, firstName: result.firstName, lastName: result.lastName, @@ -521,12 +368,10 @@ ipcMain.handle('login-agent', async (event, credentials) => { connListCount: result.connList ? result.connList.length : 0, connList: result.connList }); - - // Stocker les informations de connexion + agentConnectionInfo = result; currentTerminal = credentials.terminal; - - // Créer l'objet agent pour compatibilité + currentAgent = { id: result.accessCode, accessCode: result.accessCode, @@ -536,31 +381,32 @@ ipcMain.handle('login-agent', async (event, credentials) => { lastName: result.lastName, terminal: credentials.terminal }; - - // Traiter les URLs des applications et créer les centres + const centres = processApplicationUrls(result.connList); - - // Mettre à jour le titre de la fenêtre + if (mainWindow) { mainWindow.setTitle( `SimpleConnect v${app.getVersion()} - Agent: ${currentAgent.accessCode} (${result.firstName} ${result.lastName}) - Tel: ${credentials.terminal}` ); } - + + serverStatus = 'connected'; + sendServerStatus(); + return { success: true, agent: currentAgent, centres: centres }; } - - return { success: false, message: 'Échec de l\'authentification' }; - + + return { success: false, message: 'Echec de l\'authentification' }; + } catch (error) { console.error('Erreur lors de la connexion agent:', error); - return { - success: false, - message: error.message || 'Erreur de connexion au serveur' + return { + success: false, + message: error.message || 'Erreur de connexion au serveur' }; } }); @@ -568,10 +414,10 @@ ipcMain.handle('login-agent', async (event, credentials) => { // Traiter les URLs des applications avec les placeholders function processApplicationUrls(connList) { if (!connList || connList.length === 0) return []; - + return connList.map((conn, index) => { let url = conn.applicationName; - + // Remplacer les placeholders if (url.includes('#CA#')) { url = url.replace('#CA#', conn.accessCode || ''); @@ -579,8 +425,8 @@ function processApplicationUrls(connList) { if (url.includes('#MP#')) { url = url.replace('#MP#', conn.password || ''); } - - // Gérer les cas spécifiques des plateformes connues + + // Gerer les cas specifiques des plateformes connues if (url === 'pro.mondocteur.fr' || url.includes('mondocteur.fr')) { if (!url.startsWith('http')) { url = 'https://pro.mondocteur.fr/backoffice.do'; @@ -590,11 +436,9 @@ function processApplicationUrls(connList) { url = 'https://pro.doctolib.fr/signin'; } } else if (!url.startsWith('http')) { - // Ajouter https:// par défaut si pas de protocole url = `https://${url}`; } - - // Créer l'objet centre compatible avec l'interface + return { id: conn.code || `centre${index + 1}`, nom: conn.queueName || conn.code || `Centre ${index + 1}`, @@ -604,68 +448,54 @@ function processApplicationUrls(connList) { username: conn.accessCode, password: conn.password }, - queueName: conn.queueName // Garder pour le mapping avec les événements IPBX + queueName: conn.queueName }; }); } -// Fonction helper pour attribuer des couleurs aux centres function getColorForIndex(index) { const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#DDA0DD', '#FFD93D', '#6BCB77']; return colors[index % colors.length]; } -// Déconnexion agent via SignalR +// Deconnexion agent via Socket.IO ipcMain.handle('logout', async () => { - if (currentAgent && signalRConnection && signalRStatus === 'connected') { + if (currentAgent && adapter) { try { - // Appeler SignalR pour la déconnexion - logSignalR('📡 Invocation SignalR: AgentLogoff', { - method: 'AgentLogoff', + log('Logoff agent', { accessCode: currentAgent.accessCode }); - await signalRConnection.invoke('AgentLogoff', currentAgent.accessCode); - console.log('Agent déconnecté du serveur SignalR'); - logSignalR('👋 Agent déconnecté avec succès', { + await adapter.logoff(); + console.log('Agent deconnecte du serveur'); + log('Agent deconnecte avec succes', { accessCode: currentAgent.accessCode, name: currentAgent.name }); } catch (error) { - console.error('Erreur lors de la déconnexion SignalR:', error); + console.error('Erreur lors de la deconnexion:', error); } } - - // Réinitialiser les variables locales + currentAgent = null; currentTerminal = null; agentConnectionInfo = null; - - // Réinitialiser le titre de la fenêtre + if (mainWindow) { mainWindow.setTitle(`SimpleConnect v${app.getVersion()}`); } - + return { success: true }; }); -// Handler pour quitter l'application proprement ipcMain.handle('quit-app', async () => { - // Ne pas appeler disconnect() pour éviter l'envoi du CloseMessage - // Le serveur .NET ne supporte pas ce message - // On laisse la connexion se fermer naturellement avec l'application - // (comme le fait le client de prod) - - // Quitter l'application app.quit(); }); -// Obtenir l'agent actuel ipcMain.handle('get-current-agent', () => { if (!currentAgent || !agentConnectionInfo) return null; - - // Retourner les centres traités depuis SignalR + const centres = processApplicationUrls(agentConnectionInfo.connList); - + return { agent: currentAgent, centres: centres, @@ -673,17 +503,15 @@ ipcMain.handle('get-current-agent', () => { }; }); -// Simuler un appel entrant ipcMain.handle('simulate-call', (event, callData) => { - // Envoyer l'événement d'appel entrant à la fenêtre mainWindow.webContents.send('incoming-call', callData); return { success: true }; }); -// Obtenir les données pour simuler des appels ipcMain.handle('get-simulated-calls', () => { + if (!config.cti || !config.cti.appelSimules) return []; return config.cti.appelSimules.map(appel => { - const centre = config.centres.find(c => c.id === appel.centreId); + const centre = config.centres ? config.centres.find(c => c.id === appel.centreId) : null; return { ...appel, centreNom: centre ? centre.nom : 'Centre inconnu' @@ -691,18 +519,16 @@ ipcMain.handle('get-simulated-calls', () => { }); }); -// Sauvegarder les notes de l'agent (un seul fichier par agent) +// Sauvegarder les notes de l'agent ipcMain.handle('save-notes', (event, noteData) => { const notesDir = path.join(__dirname, 'notes'); if (!fs.existsSync(notesDir)) { fs.mkdirSync(notesDir); } - - // Un seul fichier par agent, mis à jour à chaque sauvegarde + const fileName = `notes_${currentAgent.id}.json`; const filePath = path.join(notesDir, fileName); - - // Lire l'historique existant si le fichier existe + let notesData = { agent: currentAgent.id, agentName: currentAgent.name, @@ -711,12 +537,10 @@ ipcMain.handle('save-notes', (event, noteData) => { centre: noteData.centre, history: [] }; - - // Si le fichier existe, préserver l'historique + if (fs.existsSync(filePath)) { try { const existingData = JSON.parse(fs.readFileSync(filePath, 'utf8')); - // Ajouter l'ancienne note à l'historique si elle a changé if (existingData.currentNote && existingData.currentNote !== noteData.content) { notesData.history = existingData.history || []; notesData.history.unshift({ @@ -724,27 +548,25 @@ ipcMain.handle('save-notes', (event, noteData) => { date: existingData.lastModified, centre: existingData.centre }); - // Limiter l'historique à 50 entrées notesData.history = notesData.history.slice(0, 50); } } catch (error) { console.error('Erreur lecture notes existantes:', error); } } - + fs.writeFileSync(filePath, JSON.stringify(notesData, null, 2)); - + return { success: true, file: fileName }; }); -// Récupérer les notes de l'agent ipcMain.handle('get-notes', () => { if (!currentAgent) return null; - + const notesDir = path.join(__dirname, 'notes'); const fileName = `notes_${currentAgent.id}.json`; const filePath = path.join(notesDir, fileName); - + if (fs.existsSync(filePath)) { try { const data = fs.readFileSync(filePath, 'utf8'); @@ -754,11 +576,10 @@ ipcMain.handle('get-notes', () => { return null; } } - + return null; }); -// Obtenir l'historique des appels ipcMain.handle('get-call-history', () => { const historyFile = path.join(__dirname, 'call_history.json'); if (fs.existsSync(historyFile)) { @@ -768,30 +589,27 @@ ipcMain.handle('get-call-history', () => { return []; }); -// Sauvegarder un appel dans l'historique ipcMain.handle('save-call-history', (event, callData) => { const historyFile = path.join(__dirname, 'call_history.json'); let history = []; - + if (fs.existsSync(historyFile)) { const data = fs.readFileSync(historyFile, 'utf8'); history = JSON.parse(data); } - + history.unshift({ ...callData, agentId: currentAgent?.id, timestamp: new Date().toISOString() }); - - // Garder seulement les 100 derniers appels + history = history.slice(0, 100); - + fs.writeFileSync(historyFile, JSON.stringify(history, null, 2)); return { success: true }; }); -// Vérifier si on est en mode développement ipcMain.handle('is-development', () => { return process.env.NODE_ENV === 'development'; -}); \ No newline at end of file +}); diff --git a/package.json b/package.json index d468a92..b916c8f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "simpleconnect-electron", - "version": "1.5.0", + "version": "2.0.0", "description": "Application de gestion centralisée des plannings médicaux pour centres d'appels", "main": "main.js", "scripts": { diff --git a/renderer.js b/renderer.js index 718a3f8..3e1a04c 100644 --- a/renderer.js +++ b/renderer.js @@ -29,18 +29,18 @@ document.addEventListener('DOMContentLoaded', async () => { versionLoginElement.textContent = `v${appVersion}`; } - // Initialiser l'indicateur SignalR + // Initialiser l'indicateur de statut serveur - // Écouter les changements de statut SignalR - ipcRenderer.on('signalr-status', (event, status) => { - updateSignalRIndicator(status); - // Recharger les terminaux à chaque changement de statut + // Ecouter les changements de statut serveur + ipcRenderer.on('server-status', (event, status) => { + updateServerIndicator(status); + // Recharger les terminaux a chaque changement de statut loadTerminals(); }); - // Obtenir le statut initial SignalR - const initialStatus = await ipcRenderer.invoke('get-signalr-status'); - updateSignalRIndicator(initialStatus); + // Obtenir le statut initial + const initialStatus = await ipcRenderer.invoke('get-server-status'); + updateServerIndicator(initialStatus); // Charger immédiatement les terminaux pour la page de login await loadTerminals(); @@ -129,7 +129,7 @@ document.addEventListener('DOMContentLoaded', async () => { handleIncomingCall(callData); }); - // Écouter les événements SignalR de basculement de centre + // Ecouter les evenements de basculement de centre ipcRenderer.on('switch-to-center', (event, data) => { console.log('Basculement vers le centre:', data.centreName); @@ -162,7 +162,7 @@ document.addEventListener('DOMContentLoaded', async () => { }); }); -// Connexion via SignalR +// Connexion agent async function handleLogin(e) { e.preventDefault(); @@ -221,7 +221,7 @@ async function handleLogin(e) { await new Promise(resolve => setTimeout(resolve, 300)); try { - // Préparer les credentials pour SignalR + // Preparer les credentials const credentials = { email: accessCode, // Utiliser directement le code agent comme email password: password, @@ -229,7 +229,7 @@ async function handleLogin(e) { forceDisconnect: forceDisconnect // Ajouter l'option de déconnexion forcée }; - // Appeler l'authentification SignalR + // Appeler l'authentification const result = await ipcRenderer.invoke('login-agent', credentials); if (result.success) { @@ -835,7 +835,7 @@ async function loadTerminals() { console.log('Chargement des terminaux...'); try { - // Récupérer les terminaux depuis le serveur SignalR + // Recuperer les terminaux depuis le serveur const terminals = await ipcRenderer.invoke('get-terminal-list'); availableTerminals = terminals || []; console.log(`${terminals.length} terminaux récupérés`); @@ -1235,20 +1235,20 @@ function refreshCurrentWebview() { } } -// === GESTION INDICATEUR SIGNALR === -function updateSignalRIndicator(status) { +// === GESTION INDICATEUR STATUT SERVEUR === +function updateServerIndicator(status) { const indicator = document.getElementById('signalrIndicator'); const text = document.getElementById('signalrText'); - + if (!indicator || !text) return; - - // Réinitialiser les classes + + // Reinitialiser les classes indicator.className = 'signalr-indicator'; - + switch(status) { case 'connected': indicator.classList.add('connected'); - text.textContent = 'Connecté au serveur'; + text.textContent = 'Serveur connecte'; break; case 'connecting': indicator.classList.add('connecting'); @@ -1256,17 +1256,17 @@ function updateSignalRIndicator(status) { break; case 'disconnected': indicator.classList.add('disconnected'); - text.textContent = 'Serveur déconnecté'; + text.textContent = 'Serveur deconnecte'; break; case 'error': indicator.classList.add('error'); - text.textContent = 'Erreur de connexion'; + text.textContent = 'Serveur injoignable'; break; case 'disabled': indicator.classList.add('disabled'); - text.textContent = 'SignalR désactivé'; + text.textContent = 'Non configure'; break; default: - text.textContent = 'État inconnu'; + text.textContent = 'Etat inconnu'; } } \ No newline at end of file diff --git a/socketio-adapter.js b/socketio-adapter.js new file mode 100644 index 0000000..2ff8015 --- /dev/null +++ b/socketio-adapter.js @@ -0,0 +1,151 @@ +/** + * Adaptateur Socket.IO natif pour le serveur Python SimpleServer + * + * Connexion directe au port 8004, auth au handshake, + * reconnexion native illimitee. + */ + +const io = require('socket.io-client'); + +class SocketIOAdapter { + constructor(serverUrl) { + this.serverUrl = serverUrl; + this.socket = null; + this._state = 'disconnected'; // disconnected, connecting, connected, error + this._eventHandlers = new Map(); + } + + /** + * Connexion avec auth au handshake. + * Le serveur authentifie dans le handler connect et emet 'login_ok' avec le resultat. + * @returns {Promise<object>} connResult (accessCode, firstName, lastName, connList) + */ + connect(accessCode, password, terminal) { + return new Promise((resolve, reject) => { + this._state = 'connecting'; + + this.socket = io(this.serverUrl, { + auth: { access_code: accessCode, password, terminal }, + transports: ['websocket'], + reconnection: true, + reconnectionAttempts: Infinity, + reconnectionDelay: 2000, + reconnectionDelayMax: 10000, + }); + + let settled = false; + + // Le serveur emet 'login_ok' avec les donnees de session + this.socket.once('login_ok', (data) => { + if (settled) return; + settled = true; + this._state = 'connected'; + resolve(data); + }); + + // Le serveur emet 'login_error' si auth echouee (avant return false) + this.socket.once('login_error', (data) => { + if (settled) return; + settled = true; + this._state = 'error'; + this.socket.disconnect(); + reject(new Error(data.message || 'Authentification refusee')); + }); + + // Erreur de connexion (serveur injoignable ou return false du handler connect) + this.socket.on('connect_error', (err) => { + if (!settled) { + settled = true; + this._state = 'error'; + this.socket.disconnect(); + reject(new Error(err.message || 'Connexion refusee')); + } + // Apres login reussi : reconnexion auto geree par socket.io + }); + + // Timeout de connexion initiale (15s) + setTimeout(() => { + if (!settled) { + settled = true; + this._state = 'error'; + this.socket.disconnect(); + reject(new Error('Timeout de connexion au serveur')); + } + }, 15000); + + // Restaurer les handlers enregistres avant connect + this._eventHandlers.forEach((handler, event) => { + this.socket.on(event, handler); + }); + }); + } + + /** + * Deconnexion volontaire avec logoff IPBX. + * Emet 'logout' et attend 'logout_ok' du serveur. + */ + logoff() { + return new Promise((resolve) => { + if (!this.socket || !this.socket.connected) { + this._state = 'disconnected'; + resolve(); + return; + } + + this.socket.once('logout_ok', () => { + this._state = 'disconnected'; + resolve(); + }); + + this.socket.emit('logout'); + + // Timeout si le serveur ne repond pas + setTimeout(() => { + if (this.socket) { + this.socket.disconnect(); + } + this._state = 'disconnected'; + resolve(); + }, 5000); + }); + } + + /** + * Deconnexion brute (sans logoff IPBX). + */ + disconnect() { + if (this.socket) { + this.socket.disconnect(); + } + this._state = 'disconnected'; + } + + /** + * Ecouter un evenement serveur. + */ + on(event, handler) { + this._eventHandlers.set(event, handler); + if (this.socket) { + this.socket.on(event, handler); + } + } + + /** + * Retirer un handler d'evenement. + */ + off(event) { + this._eventHandlers.delete(event); + if (this.socket) { + this.socket.off(event); + } + } + + /** + * Etat de la connexion. + */ + get state() { + return this._state; + } +} + +module.exports = SocketIOAdapter;