const { app, BrowserWindow, ipcMain, session, net } = require('electron'); const path = require('path'); const fs = require('fs'); const os = require('os'); const SocketIOAdapter = require('./socketio-adapter'); let mainWindow; let config; let currentAgent = null; let currentTerminal = null; let adapter = null; let serverStatus = 'disconnected'; // disconnected, connecting, connected, error let agentConnectionInfo = null; let healthCheckInterval = null; // Configuration du systeme de logs const LOG_DIR = path.join(os.homedir(), '.simpleconnect-ng'); const LOG_FILE = path.join(LOG_DIR, 'socketio.log'); function ensureLogDirectory() { if (!fs.existsSync(LOG_DIR)) { fs.mkdirSync(LOG_DIR, { recursive: true }); } } function stripAnsi(str) { return str.replace(/\x1b\[[0-9;]*m/g, ''); } function fileTimestamp() { return new Date().toLocaleString('fr-FR', { timeZone: 'Europe/Paris', day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' }); } function logToFile(message, data = null) { ensureLogDirectory(); let line = `[${fileTimestamp()}] ${stripAnsi(message)}`; if (data) line += ' ' + JSON.stringify(data); fs.appendFileSync(LOG_FILE, line + '\n', 'utf8'); } function logSectionToFile(message) { ensureLogDirectory(); fs.appendFileSync(LOG_FILE, `[${fileTimestamp()}] ── ${stripAnsi(message)} ──\n`, 'utf8'); } // ANSI colors const c = { reset: '\x1b[0m', dim: '\x1b[2m', green: '\x1b[32m', red: '\x1b[31m', cyan: '\x1b[36m', bold: '\x1b[1m', }; function formatTimestamp() { return new Date().toLocaleString('fr-FR', { timeZone: 'Europe/Paris', hour: '2-digit', minute: '2-digit', second: '2-digit' }); } function log(message, data = null) { console.log(`${c.dim}${formatTimestamp()}${c.reset} ${message}`); logToFile(message, data); } function logBanner(version, serverUrl) { console.log(''); console.log(` ${c.bold}${c.cyan}SimpleConnect${c.reset} ${c.dim}v${version}${c.reset}`); console.log(` ${c.dim}Serveur${c.reset} ${serverUrl}`); console.log(''); logSectionToFile(`SimpleConnect v${version} — ${serverUrl}`); } // Charger la configuration function loadConfig() { const configPath = path.join(__dirname, 'config.json'); const configData = fs.readFileSync(configPath, 'utf8'); config = JSON.parse(configData); } // Creer la fenetre principale function createWindow() { mainWindow = new BrowserWindow({ width: 1400, height: 900, webPreferences: { nodeIntegration: true, contextIsolation: false, webviewTag: true, webSecurity: false }, icon: path.join(__dirname, 'icon.png'), title: `SimpleConnect v${app.getVersion()}`, autoHideMenuBar: true }); mainWindow.setMenuBarVisibility(false); mainWindow.loadFile('index.html'); mainWindow.webContents.on('did-finish-load', () => { mainWindow.setTitle(`SimpleConnect v${app.getVersion()}`); }); if (process.env.NODE_ENV === 'development') { mainWindow.webContents.openDevTools(); } mainWindow.on('closed', () => { mainWindow = null; }); } // === GESTION SOCKET.IO === function initializeSocketIO() { if (!config.socketio || !config.socketio.serverUrl) { log('Socket.IO non configuré'); serverStatus = 'disabled'; sendServerStatus(); return; } logBanner(app.getVersion(), config.socketio.serverUrl); adapter = new SocketIOAdapter(config.socketio.serverUrl); // Demarrer le health check (ecran de login) startHealthCheck(); } // Health check periodique via GET /health // Actif uniquement sur l'ecran de login (pas d'agent connecte) function startHealthCheck() { const checkHealth = () => { // Ne pas faire de health check quand un agent est connecte // (c'est l'adapter Socket.IO qui pilote le voyant) if (currentAgent) return; const serverUrl = config.socketio.serverUrl; let done = false; const request = net.request(`${serverUrl}/health`); request.on('response', (response) => { if (done) return; done = true; const newStatus = response.statusCode === 200 ? 'connected' : 'error'; if (serverStatus !== newStatus) { serverStatus = newStatus; sendServerStatus(); } }); request.on('error', () => { if (done) return; done = true; if (serverStatus !== 'error') { serverStatus = 'error'; sendServerStatus(); } }); // Timeout 5s — net.request n'a pas de timeout natif setTimeout(() => { if (!done) { done = true; request.abort(); if (serverStatus !== 'error') { serverStatus = 'error'; sendServerStatus(); } } }, 5000); request.end(); }; // Check immediat puis toutes les 5s checkHealth(); healthCheckInterval = setInterval(checkHealth, 5000); } function stopHealthCheck() { if (healthCheckInterval) { clearInterval(healthCheckInterval); healthCheckInterval = null; } } function sendServerStatus() { if (serverStatus !== 'connected') { log(`${c.red}✗${c.reset} Serveur injoignable`); } if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('server-status', serverStatus); } } // Gerer un appel entrant function handleCallPickedUp(event) { if (!mainWindow || !agentConnectionInfo) return; const centres = processApplicationUrls(agentConnectionInfo.connList); const centre = centres.find(c => c.queueName === event.queueName); if (centre) { log(`Basculement vers le centre: ${centre.nom}`); mainWindow.webContents.send('switch-to-center', { centreId: centre.id, centreName: centre.nom, queueName: event.queueName, terminal: event.terminal, eventType: 'call_pickup' }); } else { log(`${c.yellow}Aucun centre trouvé pour la file: ${event.queueName}${c.reset}`); } } // Gerer la fin d'un appel function handleCallHungUp(event) { if (!mainWindow) return; log(`Fin d'appel sur la file: ${event.queueName}`); mainWindow.webContents.send('release-center', { queueName: event.queueName, terminal: event.terminal, eventType: 'call_hangup' }); } // Configurer les handlers d'evenements Socket.IO apres connexion function setupEventHandlers() { if (!adapter) return; log('Session Socket.IO démarrée', { 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) { log(`Événement ignoré — terminal différent: ${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(() => { const { Menu } = require('electron'); Menu.setApplicationMenu(null); 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 }); }); session.defaultSession.webRequest.onHeadersReceived((details, callback) => { callback({ responseHeaders: { ...details.responseHeaders, 'Content-Security-Policy': ['default-src * \'unsafe-inline\' \'unsafe-eval\' data: blob:;'] } }); }); loadConfig(); createWindow(); initializeSocketIO(); }); // Quitter quand toutes les fenetres sont fermees app.on('window-all-closed', async () => { if (healthCheckInterval) { clearInterval(healthCheckInterval); } // Déconnexion propre avant de quitter if (currentAgent && adapter) { try { await adapter.logoff(); } catch (error) { log(`${c.red}Erreur déconnexion: ${error.message}${c.reset}`); } } if (process.platform !== 'darwin') { log(`${c.dim}Application fermée${c.reset}`); logSectionToFile('Application fermée'); app.quit(); } }); app.on('activate', () => { if (mainWindow === null) { createWindow(); } }); // === IPC HANDLERS === ipcMain.handle('get-config', () => { return config; }); ipcMain.handle('get-app-version', () => { return app.getVersion(); }); ipcMain.handle('get-server-status', () => { return serverStatus; }); // Recuperer la liste des terminaux via REST ipcMain.handle('get-terminal-list', async () => { if (!config.socketio || !config.socketio.serverUrl) { return []; } try { const provider = config.socketio.serviceProvider; const url = `${config.socketio.serverUrl}/terminals?provider=${encodeURIComponent(provider)}`; 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(`${c.green}✓${c.reset} Serveur connecté — ${terminals.length} terminaux`); resolve(Array.isArray(terminals) ? terminals : []); } catch (e) { log(`${c.red}Erreur parsing terminaux: ${e.message}${c.reset}`); resolve([]); } } else { log(`${c.red}Erreur terminaux HTTP ${response.statusCode}${c.reset}`); resolve([]); } }); }); request.on('error', (error) => { log(`${c.red}Erreur récupération terminaux: ${error.message}${c.reset}`); resolve([]); }); request.end(); }); } catch (error) { log(`${c.red}Erreur récupération terminaux: ${error.message}${c.reset}`); return []; } }); // Connexion agent via Socket.IO ipcMain.handle('login-agent', async (event, credentials) => { if (!adapter) { return { success: false, message: 'Socket.IO non initialise. Veuillez reessayer.' }; } try { log(`Tentative de connexion agent: ${credentials.email} Terminal: ${credentials.terminal}`); // Deconnecter l'adapter precedent s'il y en a un if (adapter.state === 'connected') { adapter.disconnect(); } // Recreer l'adapter pour une connexion fraiche adapter = new SocketIOAdapter(config.socketio.serverUrl); // L'adapter pilote le voyant une fois connecte adapter.onStateChange((state) => { log('Etat Socket.IO change', { state }); switch (state) { case 'reconnecting': serverStatus = 'connecting'; sendServerStatus(); break; case 'connected': serverStatus = 'connected'; sendServerStatus(); break; case 'error': case 'disconnected': serverStatus = 'error'; sendServerStatus(); break; } }); // 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) { log(`${c.green}✓${c.reset} Connexion réussie: ${result.accessCode} (${result.firstName} ${result.lastName})`); log('Connexion agent réussie', { accessCode: result.accessCode, firstName: result.firstName, lastName: result.lastName, terminal: credentials.terminal, connListCount: result.connList ? result.connList.length : 0, connList: result.connList }); agentConnectionInfo = result; currentTerminal = credentials.terminal; currentAgent = { id: result.accessCode, accessCode: result.accessCode, name: `${result.firstName} ${result.lastName}`, email: credentials.email, firstName: result.firstName, lastName: result.lastName, terminal: credentials.terminal }; const centres = processApplicationUrls(result.connList); if (mainWindow) { mainWindow.setTitle( `SimpleConnect v${app.getVersion()} - Agent: ${currentAgent.accessCode} (${result.firstName} ${result.lastName}) - Tel: ${credentials.terminal}` ); } // Agent connecte → l'adapter pilote le voyant, on arrete le health check stopHealthCheck(); serverStatus = 'connected'; sendServerStatus(); return { success: true, agent: currentAgent, centres: centres }; } return { success: false, message: 'Echec de l\'authentification' }; } catch (error) { log(`${c.red}Erreur connexion agent: ${error.message}${c.reset}`); return { success: false, message: error.message || 'Erreur de connexion au serveur' }; } }); // 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 || ''); } if (url.includes('#MP#')) { url = url.replace('#MP#', conn.password || ''); } // 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'; } } else if (url === 'pro.doctolib.fr' || url.includes('doctolib.fr')) { if (!url.startsWith('http')) { url = 'https://pro.doctolib.fr/signin'; } } else if (!url.startsWith('http')) { url = `https://${url}`; } return { id: conn.code || `centre${index + 1}`, nom: conn.queueName || conn.code || `Centre ${index + 1}`, url: url, couleur: getColorForIndex(index), credentials: { username: conn.accessCode, password: conn.password }, queueName: conn.queueName }; }); } function getColorForIndex(index) { const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#DDA0DD', '#FFD93D', '#6BCB77']; return colors[index % colors.length]; } // Deconnexion agent via Socket.IO ipcMain.handle('logout', async () => { if (currentAgent && adapter) { try { log('Logoff agent', { accessCode: currentAgent.accessCode }); await adapter.logoff(); log('Agent déconnecté du serveur'); log('Agent déconnecté avec succès', { accessCode: currentAgent.accessCode, name: currentAgent.name }); } catch (error) { log(`${c.red}Erreur déconnexion: ${error.message}${c.reset}`); } } currentAgent = null; currentTerminal = null; agentConnectionInfo = null; // Retour ecran login → relancer le health check startHealthCheck(); if (mainWindow) { mainWindow.setTitle(`SimpleConnect v${app.getVersion()}`); } return { success: true }; }); ipcMain.handle('quit-app', async () => { log(`${c.dim}Application fermée${c.reset}`); logSectionToFile('Application fermée'); app.quit(); }); ipcMain.handle('get-current-agent', () => { if (!currentAgent || !agentConnectionInfo) return null; const centres = processApplicationUrls(agentConnectionInfo.connList); return { agent: currentAgent, centres: centres, terminal: currentTerminal }; }); ipcMain.handle('simulate-call', (event, callData) => { mainWindow.webContents.send('incoming-call', callData); return { success: true }; }); ipcMain.handle('get-simulated-calls', () => { if (!config.cti || !config.cti.appelSimules) return []; return config.cti.appelSimules.map(appel => { const centre = config.centres ? config.centres.find(c => c.id === appel.centreId) : null; return { ...appel, centreNom: centre ? centre.nom : 'Centre inconnu' }; }); }); // 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); } const fileName = `notes_${currentAgent.id}.json`; const filePath = path.join(notesDir, fileName); let notesData = { agent: currentAgent.id, agentName: currentAgent.name, currentNote: noteData.content, lastModified: new Date().toISOString(), centre: noteData.centre, history: [] }; if (fs.existsSync(filePath)) { try { const existingData = JSON.parse(fs.readFileSync(filePath, 'utf8')); if (existingData.currentNote && existingData.currentNote !== noteData.content) { notesData.history = existingData.history || []; notesData.history.unshift({ content: existingData.currentNote, date: existingData.lastModified, centre: existingData.centre }); notesData.history = notesData.history.slice(0, 50); } } catch (error) { log(`${c.red}Erreur lecture notes existantes: ${error.message}${c.reset}`); } } fs.writeFileSync(filePath, JSON.stringify(notesData, null, 2)); return { success: true, file: fileName }; }); 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'); return JSON.parse(data); } catch (error) { log(`${c.red}Erreur lecture notes: ${error.message}${c.reset}`); return null; } } return null; }); ipcMain.handle('get-call-history', () => { const historyFile = path.join(__dirname, 'call_history.json'); if (fs.existsSync(historyFile)) { const data = fs.readFileSync(historyFile, 'utf8'); return JSON.parse(data); } return []; }); 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() }); history = history.slice(0, 100); fs.writeFileSync(historyFile, JSON.stringify(history, null, 2)); return { success: true }; }); ipcMain.handle('is-development', () => { return process.env.NODE_ENV === 'development'; });