feat: migration Socket.IO natif — login, terminaux REST, health check (closes #3)

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
This commit is contained in:
Pierre Marx
2026-03-18 17:31:30 -04:00
parent 630f1fa8c3
commit 77a310976b
5 changed files with 414 additions and 447 deletions

View File

@@ -1,8 +1,6 @@
{ {
"signalR": { "socketio": {
"enabled": true, "serverUrl": "http://10.90.20.201:8004",
"serverUrl": "http://10.90.20.201:8002/signalR", "serviceProvider": "RDVPREM"
"serviceProvider": "RDVPREM",
"terminalsSimulation": ["3001", "3002", "3003", "3004", "3005"]
} }
} }

542
main.js
View File

@@ -1,32 +1,29 @@
const { app, BrowserWindow, ipcMain, session } = require('electron'); const { app, BrowserWindow, ipcMain, session, net } = require('electron');
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const os = require('os'); const os = require('os');
const signalR = require('@microsoft/signalr'); const SocketIOAdapter = require('./socketio-adapter');
const ConnectionManager = require('./connection-manager');
let mainWindow; let mainWindow;
let config; let config;
let currentAgent = null; let currentAgent = null;
let currentTerminal = null; // Terminal téléphonique de l'agent connecté let currentTerminal = null;
let signalRConnection = null; let adapter = null;
let signalRStatus = 'disconnected'; // disconnected, connecting, connected, error let serverStatus = 'disconnected'; // disconnected, connecting, connected, error
let agentConnectionInfo = null; // Informations complètes retournées par SignalR let agentConnectionInfo = null;
let healthCheckInterval = null;
// Configuration du système de logs SignalR // Configuration du systeme de logs
const SIGNALR_LOG_DIR = path.join(os.homedir(), '.simpleconnect-ng'); const LOG_DIR = path.join(os.homedir(), '.simpleconnect-ng');
const SIGNALR_LOG_FILE = path.join(SIGNALR_LOG_DIR, 'signalr.log'); const LOG_FILE = path.join(LOG_DIR, 'socketio.log');
// Créer le répertoire de logs s'il n'existe pas
function ensureLogDirectory() { function ensureLogDirectory() {
if (!fs.existsSync(SIGNALR_LOG_DIR)) { if (!fs.existsSync(LOG_DIR)) {
fs.mkdirSync(SIGNALR_LOG_DIR, { recursive: true }); fs.mkdirSync(LOG_DIR, { recursive: true });
console.log(`📁 Répertoire de logs créé: ${SIGNALR_LOG_DIR}`);
} }
} }
// Fonction pour écrire dans le fichier de log SignalR function logToFile(message, data = null) {
function logToSignalRFile(message, data = null) {
ensureLogDirectory(); ensureLogDirectory();
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
@@ -36,16 +33,14 @@ function logToSignalRFile(message, data = null) {
logEntry += '\n' + JSON.stringify(data, null, 2); logEntry += '\n' + JSON.stringify(data, null, 2);
} }
logEntry += '\n' + ''.repeat(80) + '\n'; logEntry += '\n' + '-'.repeat(80) + '\n';
// Ajouter au fichier (append) fs.appendFileSync(LOG_FILE, logEntry, 'utf8');
fs.appendFileSync(SIGNALR_LOG_FILE, logEntry, 'utf8');
} }
// Logger dans la console ET dans le fichier function log(message, data = null) {
function logSignalR(message, data = null) {
console.log(message, data || ''); console.log(message, data || '');
logToSignalRFile(message, data); logToFile(message, data);
} }
// Charger la configuration // Charger la configuration
@@ -55,7 +50,7 @@ function loadConfig() {
config = JSON.parse(configData); config = JSON.parse(configData);
} }
// Créer la fenêtre principale // Creer la fenetre principale
function createWindow() { function createWindow() {
mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
width: 1400, width: 1400,
@@ -68,211 +63,92 @@ function createWindow() {
}, },
icon: path.join(__dirname, 'icon.png'), icon: path.join(__dirname, 'icon.png'),
title: `SimpleConnect v${app.getVersion()}`, 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); mainWindow.setMenuBarVisibility(false);
// Charger l'interface HTML
mainWindow.loadFile('index.html'); mainWindow.loadFile('index.html');
// Forcer le titre après le chargement de la page (le <title> HTML l'écrase sinon)
mainWindow.webContents.on('did-finish-load', () => { mainWindow.webContents.on('did-finish-load', () => {
mainWindow.setTitle(`SimpleConnect v${app.getVersion()}`); mainWindow.setTitle(`SimpleConnect v${app.getVersion()}`);
}); });
// Ouvrir les DevTools uniquement en mode développement
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
mainWindow.webContents.openDevTools(); mainWindow.webContents.openDevTools();
} }
// Gérer la fermeture de la fenêtre
mainWindow.on('closed', () => { mainWindow.on('closed', () => {
mainWindow = null; mainWindow = null;
}); });
} }
// === GESTION SIGNALR/WEBSOCKET === // === GESTION SOCKET.IO ===
function initializeSignalR() {
if (!config.signalR || !config.signalR.enabled) { function initializeSocketIO() {
console.log('SignalR/WebSocket désactivé dans la configuration'); if (!config.socketio || !config.socketio.serverUrl) {
signalRStatus = 'disabled'; console.log('Socket.IO non configure');
sendSignalRStatus(); serverStatus = 'disabled';
sendServerStatus();
return; return;
} }
try { adapter = new SocketIOAdapter(config.socketio.serverUrl);
// 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 // Demarrer le health check polling
signalRConnection = connectionManager; startHealthCheck();
// 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();
}
} }
function setupSignalRHandlers() { // Health check periodique via GET /health
// Configuration des handlers après la connexion function startHealthCheck() {
const connection = signalRConnection.getConnection(); const checkHealth = () => {
if (!connection) { const serverUrl = config.socketio.serverUrl;
console.error('Pas de connexion active pour configurer les handlers');
return; 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();
} }
// === 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('═══════════════════════════════════════════════════════════'); request.on('error', () => {
serverStatus = 'error';
sendServerStatus();
});
// Logger dans le fichier avec structure request.end();
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); // Check immediat puis toutes les 10s
checkHealth();
// Si c'est IpbxEvent, traiter comme avant healthCheckInterval = setInterval(checkHealth, 10000);
if (eventName === 'IpbxEvent') {
handleIpbxEventOriginal(args);
}
});
});
// 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
});
// 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');
} }
// Gérer un appel entrant function sendServerStatus() {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('server-status', serverStatus);
}
}
// Gerer un appel entrant
function handleCallPickedUp(event) { function handleCallPickedUp(event) {
if (!mainWindow || !agentConnectionInfo) return; if (!mainWindow || !agentConnectionInfo) return;
// Identifier le centre correspondant à la file
const centres = processApplicationUrls(agentConnectionInfo.connList); const centres = processApplicationUrls(agentConnectionInfo.connList);
const centre = centres.find(c => c.queueName === event.queueName); const centre = centres.find(c => c.queueName === event.queueName);
if (centre) { if (centre) {
console.log('Basculement vers le centre:', centre.nom); console.log('Basculement vers le centre:', centre.nom);
// Envoyer l'instruction de basculement à la fenêtre
mainWindow.webContents.send('switch-to-center', { mainWindow.webContents.send('switch-to-center', {
centreId: centre.id, centreId: centre.id,
centreName: centre.nom, centreName: centre.nom,
@@ -281,17 +157,15 @@ function handleCallPickedUp(event) {
eventType: 'call_pickup' eventType: 'call_pickup'
}); });
} else { } 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) { function handleCallHungUp(event) {
if (!mainWindow) return; if (!mainWindow) return;
console.log('Fin d\'appel sur la file:', event.queueName); console.log('Fin d\'appel sur la file:', event.queueName);
// Envoyer l'instruction de libération à la fenêtre
mainWindow.webContents.send('release-center', { mainWindow.webContents.send('release-center', {
queueName: event.queueName, queueName: event.queueName,
terminal: event.terminal, terminal: event.terminal,
@@ -299,68 +173,45 @@ function handleCallHungUp(event) {
}); });
} }
async function startSignalRConnection() { // Configurer les handlers d'evenements Socket.IO apres connexion
try { function setupEventHandlers() {
signalRStatus = 'connecting'; if (!adapter) return;
sendSignalRStatus();
logSignalR('🔌 Tentative de connexion au serveur...', { log('Session Socket.IO demarree', {
serverUrl: config.ServerIp || config.signalR.serverUrl, serverUrl: config.socketio.serverUrl,
status: 'connecting' serviceProvider: config.socketio.serviceProvider
}); });
// Le ConnectionManager gère le fallback automatiquement // Ecouter les evenements d'appels IPBX
const connection = await signalRConnection.connect(); adapter.on('ipbx_event', (data) => {
log('ipbx_event recu', data);
// Déterminer quel type de connexion a réussi if (!agentConnectionInfo) return;
const connectionInfo = signalRConnection.getConnectionInfo();
console.log(`Connexion établie via ${connectionInfo.type}`); // Verifier que l'evenement est pour notre terminal
logSignalR(`✅ Connexion établie avec succès (${connectionInfo.type})`, { if (data.terminal !== currentTerminal) {
connectionType: connectionInfo.type, console.log('Evenement ignore - terminal different:', data.terminal, '!==', currentTerminal);
isSignalR: connectionInfo.isSignalR, return;
isRestSocketIO: connectionInfo.isRestSocketIO, }
status: 'connected',
serverUrl: connectionInfo.serverUrl switch (data.eventCode) {
case 1:
handleCallPickedUp(data);
break;
case 2:
handleCallHungUp(data);
break;
default:
log('Code evenement non gere:', data);
}
}); });
// 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);
}
}
function sendSignalRStatus() {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('signalr-status', signalRStatus);
}
} }
// Initialisation de l'application // Initialisation de l'application
app.whenReady().then(() => { app.whenReady().then(() => {
// Supprimer le menu de l'application complètement (toutes plateformes)
const { Menu } = require('electron'); const { Menu } = require('electron');
Menu.setApplicationMenu(null); Menu.setApplicationMenu(null);
// Configuration de la session pour éviter les problèmes CORS
session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => { 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'; 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 }); callback({ requestHeaders: details.requestHeaders });
@@ -377,33 +228,30 @@ app.whenReady().then(() => {
loadConfig(); loadConfig();
createWindow(); createWindow();
initializeSocketIO();
// Initialiser SignalR après le chargement de la config
initializeSignalR();
}); });
// Quitter quand toutes les fenêtres sont fermées // Quitter quand toutes les fenetres sont fermees
app.on('window-all-closed', async () => { app.on('window-all-closed', async () => {
// Déconnexion propre avant de quitter if (healthCheckInterval) {
if (currentAgent && signalRConnection && signalRStatus === 'connected') { clearInterval(healthCheckInterval);
try {
await signalRConnection.invoke('AgentLogoff', currentAgent.accessCode);
console.log('Déconnexion agent avant fermeture');
} catch (error) {
console.error('Erreur déconnexion:', error);
}
} }
// Ne pas appeler disconnect() pour éviter l'envoi du CloseMessage // Deconnexion propre avant de quitter
// Le serveur .NET ne supporte pas ce message if (currentAgent && adapter) {
// On laisse la connexion se fermer naturellement avec l'application try {
await adapter.logoff();
console.log('Agent deconnecte avant fermeture');
} catch (error) {
console.error('Erreur deconnexion:', error);
}
}
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
app.quit(); app.quit();
} }
}); });
// Réactiver l'app sur macOS
app.on('activate', () => { app.on('activate', () => {
if (mainWindow === null) { if (mainWindow === null) {
createWindow(); createWindow();
@@ -412,108 +260,107 @@ app.on('activate', () => {
// === IPC HANDLERS === // === IPC HANDLERS ===
// Obtenir la configuration
ipcMain.handle('get-config', () => { ipcMain.handle('get-config', () => {
return config; return config;
}); });
// Obtenir la version de l'application
ipcMain.handle('get-app-version', () => { ipcMain.handle('get-app-version', () => {
return app.getVersion(); return app.getVersion();
}); });
// Obtenir le statut SignalR // Renomme : get-signalr-status -> get-server-status
ipcMain.handle('get-signalr-status', () => { ipcMain.handle('get-server-status', () => {
return signalRStatus; 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 () => { ipcMain.handle('get-terminal-list', async () => {
// Mode simulation si SignalR non connecté if (!config.socketio || !config.socketio.serverUrl) {
if (!signalRConnection || signalRStatus !== 'connected') { return [];
console.log('SignalR non connecté, utilisation des terminaux de simulation');
return config.signalR.terminalsSimulation || ['3001', '3002', '3003'];
} }
try { try {
console.log('Récupération des terminaux pour:', config.signalR.serviceProvider); const provider = config.socketio.serviceProvider;
logSignalR('📡 Invocation SignalR: GetTerminalListByServiceProvider', { const url = `${config.socketio.serverUrl}/terminals?provider=${encodeURIComponent(provider)}`;
method: 'GetTerminalListByServiceProvider', console.log('Recuperation des terminaux:', url);
serviceProvider: config.signalR.serviceProvider
return new Promise((resolve, reject) => {
const request = net.request(url);
let body = '';
request.on('response', (response) => {
response.on('data', (chunk) => {
body += chunk.toString();
}); });
const terminals = await signalRConnection.invoke( response.on('end', () => {
'GetTerminalListByServiceProvider', if (response.statusCode === 200) {
config.signalR.serviceProvider try {
); const terminals = JSON.parse(body);
log('Terminaux recuperes', { count: terminals.length, terminals });
console.log('Terminaux disponibles:', terminals); resolve(Array.isArray(terminals) ? terminals : []);
logSignalR('📞 Terminaux récupérés', { } catch (e) {
count: terminals.length, console.error('Erreur parsing terminaux:', e);
terminals: terminals resolve([]);
}
} else {
console.error('Erreur terminaux HTTP', response.statusCode);
resolve([]);
}
});
});
request.on('error', (error) => {
console.error('Erreur recuperation terminaux:', error);
resolve([]);
});
request.end();
}); });
return terminals || [];
} catch (error) { } catch (error) {
console.error('Erreur récupération terminaux:', error); console.error('Erreur recuperation terminaux:', error);
// Retourner les terminaux de simulation en cas d'erreur return [];
return config.signalR.terminalsSimulation || ['3001', '3002', '3003'];
} }
}); });
// Connexion agent via SignalR // Connexion agent via Socket.IO
ipcMain.handle('login-agent', async (event, credentials) => { ipcMain.handle('login-agent', async (event, credentials) => {
// Vérifier que SignalR est connecté if (!adapter) {
if (!signalRConnection || signalRStatus !== 'connected') {
return { return {
success: false, success: false,
message: 'Connexion au serveur SignalR non établie. Veuillez réessayer.' message: 'Socket.IO non initialise. Veuillez reessayer.'
}; };
} }
try { try {
console.log('Tentative de connexion agent:', credentials.email, 'Terminal:', credentials.terminal); console.log('Tentative de connexion agent:', credentials.email, 'Terminal:', credentials.terminal);
logSignalR('🔐 Tentative de connexion agent', { log('Tentative de connexion agent', {
email: credentials.email, email: credentials.email,
terminal: credentials.terminal, terminal: credentials.terminal,
forceDisconnect: credentials.forceDisconnect || false forceDisconnect: credentials.forceDisconnect || false
}); });
// Si déconnexion forcée demandée, déconnecter d'abord la session précédente // Deconnecter l'adapter precedent s'il y en a un
if (credentials.forceDisconnect) { if (adapter.state === 'connected') {
console.log('Déconnexion forcée demandée pour:', credentials.email); adapter.disconnect();
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
}
} }
// Appel SignalR pour l'authentification // Recreer l'adapter pour une connexion fraiche
logSignalR('📡 Invocation SignalR: AgentLogin', { adapter = new SocketIOAdapter(config.socketio.serverUrl);
method: 'AgentLogin',
email: credentials.email,
terminal: credentials.terminal
});
const result = await signalRConnection.invoke('AgentLogin', // Configurer les handlers d'evenements AVANT connect
setupEventHandlers();
// Connexion avec auth au handshake
const result = await adapter.connect(
credentials.email, credentials.email,
credentials.password, credentials.password,
credentials.terminal credentials.terminal
); );
if (result) { if (result) {
console.log('Connexion réussie:', result); console.log('Connexion reussie:', result);
logSignalR('Connexion agent réussie', { log('Connexion agent reussie', {
accessCode: result.accessCode, accessCode: result.accessCode,
firstName: result.firstName, firstName: result.firstName,
lastName: result.lastName, lastName: result.lastName,
@@ -522,11 +369,9 @@ ipcMain.handle('login-agent', async (event, credentials) => {
connList: result.connList connList: result.connList
}); });
// Stocker les informations de connexion
agentConnectionInfo = result; agentConnectionInfo = result;
currentTerminal = credentials.terminal; currentTerminal = credentials.terminal;
// Créer l'objet agent pour compatibilité
currentAgent = { currentAgent = {
id: result.accessCode, id: result.accessCode,
accessCode: result.accessCode, accessCode: result.accessCode,
@@ -537,16 +382,17 @@ ipcMain.handle('login-agent', async (event, credentials) => {
terminal: credentials.terminal terminal: credentials.terminal
}; };
// Traiter les URLs des applications et créer les centres
const centres = processApplicationUrls(result.connList); const centres = processApplicationUrls(result.connList);
// Mettre à jour le titre de la fenêtre
if (mainWindow) { if (mainWindow) {
mainWindow.setTitle( mainWindow.setTitle(
`SimpleConnect v${app.getVersion()} - Agent: ${currentAgent.accessCode} (${result.firstName} ${result.lastName}) - Tel: ${credentials.terminal}` `SimpleConnect v${app.getVersion()} - Agent: ${currentAgent.accessCode} (${result.firstName} ${result.lastName}) - Tel: ${credentials.terminal}`
); );
} }
serverStatus = 'connected';
sendServerStatus();
return { return {
success: true, success: true,
agent: currentAgent, agent: currentAgent,
@@ -554,7 +400,7 @@ ipcMain.handle('login-agent', async (event, credentials) => {
}; };
} }
return { success: false, message: 'Échec de l\'authentification' }; return { success: false, message: 'Echec de l\'authentification' };
} catch (error) { } catch (error) {
console.error('Erreur lors de la connexion agent:', error); console.error('Erreur lors de la connexion agent:', error);
@@ -580,7 +426,7 @@ function processApplicationUrls(connList) {
url = url.replace('#MP#', conn.password || ''); 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 === 'pro.mondocteur.fr' || url.includes('mondocteur.fr')) {
if (!url.startsWith('http')) { if (!url.startsWith('http')) {
url = 'https://pro.mondocteur.fr/backoffice.do'; url = 'https://pro.mondocteur.fr/backoffice.do';
@@ -590,11 +436,9 @@ function processApplicationUrls(connList) {
url = 'https://pro.doctolib.fr/signin'; url = 'https://pro.doctolib.fr/signin';
} }
} else if (!url.startsWith('http')) { } else if (!url.startsWith('http')) {
// Ajouter https:// par défaut si pas de protocole
url = `https://${url}`; url = `https://${url}`;
} }
// Créer l'objet centre compatible avec l'interface
return { return {
id: conn.code || `centre${index + 1}`, id: conn.code || `centre${index + 1}`,
nom: conn.queueName || conn.code || `Centre ${index + 1}`, nom: conn.queueName || conn.code || `Centre ${index + 1}`,
@@ -604,43 +448,38 @@ function processApplicationUrls(connList) {
username: conn.accessCode, username: conn.accessCode,
password: conn.password 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) { function getColorForIndex(index) {
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#DDA0DD', '#FFD93D', '#6BCB77']; const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#DDA0DD', '#FFD93D', '#6BCB77'];
return colors[index % colors.length]; return colors[index % colors.length];
} }
// Déconnexion agent via SignalR // Deconnexion agent via Socket.IO
ipcMain.handle('logout', async () => { ipcMain.handle('logout', async () => {
if (currentAgent && signalRConnection && signalRStatus === 'connected') { if (currentAgent && adapter) {
try { try {
// Appeler SignalR pour la déconnexion log('Logoff agent', {
logSignalR('📡 Invocation SignalR: AgentLogoff', {
method: 'AgentLogoff',
accessCode: currentAgent.accessCode accessCode: currentAgent.accessCode
}); });
await signalRConnection.invoke('AgentLogoff', currentAgent.accessCode); await adapter.logoff();
console.log('Agent déconnecté du serveur SignalR'); console.log('Agent deconnecte du serveur');
logSignalR('👋 Agent déconnecté avec succès', { log('Agent deconnecte avec succes', {
accessCode: currentAgent.accessCode, accessCode: currentAgent.accessCode,
name: currentAgent.name name: currentAgent.name
}); });
} catch (error) { } 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; currentAgent = null;
currentTerminal = null; currentTerminal = null;
agentConnectionInfo = null; agentConnectionInfo = null;
// Réinitialiser le titre de la fenêtre
if (mainWindow) { if (mainWindow) {
mainWindow.setTitle(`SimpleConnect v${app.getVersion()}`); mainWindow.setTitle(`SimpleConnect v${app.getVersion()}`);
} }
@@ -648,22 +487,13 @@ ipcMain.handle('logout', async () => {
return { success: true }; return { success: true };
}); });
// Handler pour quitter l'application proprement
ipcMain.handle('quit-app', async () => { 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(); app.quit();
}); });
// Obtenir l'agent actuel
ipcMain.handle('get-current-agent', () => { ipcMain.handle('get-current-agent', () => {
if (!currentAgent || !agentConnectionInfo) return null; if (!currentAgent || !agentConnectionInfo) return null;
// Retourner les centres traités depuis SignalR
const centres = processApplicationUrls(agentConnectionInfo.connList); const centres = processApplicationUrls(agentConnectionInfo.connList);
return { return {
@@ -673,17 +503,15 @@ ipcMain.handle('get-current-agent', () => {
}; };
}); });
// Simuler un appel entrant
ipcMain.handle('simulate-call', (event, callData) => { ipcMain.handle('simulate-call', (event, callData) => {
// Envoyer l'événement d'appel entrant à la fenêtre
mainWindow.webContents.send('incoming-call', callData); mainWindow.webContents.send('incoming-call', callData);
return { success: true }; return { success: true };
}); });
// Obtenir les données pour simuler des appels
ipcMain.handle('get-simulated-calls', () => { ipcMain.handle('get-simulated-calls', () => {
if (!config.cti || !config.cti.appelSimules) return [];
return config.cti.appelSimules.map(appel => { 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 { return {
...appel, ...appel,
centreNom: centre ? centre.nom : 'Centre inconnu' 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) => { ipcMain.handle('save-notes', (event, noteData) => {
const notesDir = path.join(__dirname, 'notes'); const notesDir = path.join(__dirname, 'notes');
if (!fs.existsSync(notesDir)) { if (!fs.existsSync(notesDir)) {
fs.mkdirSync(notesDir); fs.mkdirSync(notesDir);
} }
// Un seul fichier par agent, mis à jour à chaque sauvegarde
const fileName = `notes_${currentAgent.id}.json`; const fileName = `notes_${currentAgent.id}.json`;
const filePath = path.join(notesDir, fileName); const filePath = path.join(notesDir, fileName);
// Lire l'historique existant si le fichier existe
let notesData = { let notesData = {
agent: currentAgent.id, agent: currentAgent.id,
agentName: currentAgent.name, agentName: currentAgent.name,
@@ -712,11 +538,9 @@ ipcMain.handle('save-notes', (event, noteData) => {
history: [] history: []
}; };
// Si le fichier existe, préserver l'historique
if (fs.existsSync(filePath)) { if (fs.existsSync(filePath)) {
try { try {
const existingData = JSON.parse(fs.readFileSync(filePath, 'utf8')); 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) { if (existingData.currentNote && existingData.currentNote !== noteData.content) {
notesData.history = existingData.history || []; notesData.history = existingData.history || [];
notesData.history.unshift({ notesData.history.unshift({
@@ -724,7 +548,6 @@ ipcMain.handle('save-notes', (event, noteData) => {
date: existingData.lastModified, date: existingData.lastModified,
centre: existingData.centre centre: existingData.centre
}); });
// Limiter l'historique à 50 entrées
notesData.history = notesData.history.slice(0, 50); notesData.history = notesData.history.slice(0, 50);
} }
} catch (error) { } catch (error) {
@@ -737,7 +560,6 @@ ipcMain.handle('save-notes', (event, noteData) => {
return { success: true, file: fileName }; return { success: true, file: fileName };
}); });
// Récupérer les notes de l'agent
ipcMain.handle('get-notes', () => { ipcMain.handle('get-notes', () => {
if (!currentAgent) return null; if (!currentAgent) return null;
@@ -758,7 +580,6 @@ ipcMain.handle('get-notes', () => {
return null; return null;
}); });
// Obtenir l'historique des appels
ipcMain.handle('get-call-history', () => { ipcMain.handle('get-call-history', () => {
const historyFile = path.join(__dirname, 'call_history.json'); const historyFile = path.join(__dirname, 'call_history.json');
if (fs.existsSync(historyFile)) { if (fs.existsSync(historyFile)) {
@@ -768,7 +589,6 @@ ipcMain.handle('get-call-history', () => {
return []; return [];
}); });
// Sauvegarder un appel dans l'historique
ipcMain.handle('save-call-history', (event, callData) => { ipcMain.handle('save-call-history', (event, callData) => {
const historyFile = path.join(__dirname, 'call_history.json'); const historyFile = path.join(__dirname, 'call_history.json');
let history = []; let history = [];
@@ -784,14 +604,12 @@ ipcMain.handle('save-call-history', (event, callData) => {
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
}); });
// Garder seulement les 100 derniers appels
history = history.slice(0, 100); history = history.slice(0, 100);
fs.writeFileSync(historyFile, JSON.stringify(history, null, 2)); fs.writeFileSync(historyFile, JSON.stringify(history, null, 2));
return { success: true }; return { success: true };
}); });
// Vérifier si on est en mode développement
ipcMain.handle('is-development', () => { ipcMain.handle('is-development', () => {
return process.env.NODE_ENV === 'development'; return process.env.NODE_ENV === 'development';
}); });

View File

@@ -1,6 +1,6 @@
{ {
"name": "simpleconnect-electron", "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", "description": "Application de gestion centralisée des plannings médicaux pour centres d'appels",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {

View File

@@ -29,18 +29,18 @@ document.addEventListener('DOMContentLoaded', async () => {
versionLoginElement.textContent = `v${appVersion}`; versionLoginElement.textContent = `v${appVersion}`;
} }
// Initialiser l'indicateur SignalR // Initialiser l'indicateur de statut serveur
// Écouter les changements de statut SignalR // Ecouter les changements de statut serveur
ipcRenderer.on('signalr-status', (event, status) => { ipcRenderer.on('server-status', (event, status) => {
updateSignalRIndicator(status); updateServerIndicator(status);
// Recharger les terminaux à chaque changement de statut // Recharger les terminaux a chaque changement de statut
loadTerminals(); loadTerminals();
}); });
// Obtenir le statut initial SignalR // Obtenir le statut initial
const initialStatus = await ipcRenderer.invoke('get-signalr-status'); const initialStatus = await ipcRenderer.invoke('get-server-status');
updateSignalRIndicator(initialStatus); updateServerIndicator(initialStatus);
// Charger immédiatement les terminaux pour la page de login // Charger immédiatement les terminaux pour la page de login
await loadTerminals(); await loadTerminals();
@@ -129,7 +129,7 @@ document.addEventListener('DOMContentLoaded', async () => {
handleIncomingCall(callData); 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) => { ipcRenderer.on('switch-to-center', (event, data) => {
console.log('Basculement vers le centre:', data.centreName); 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) { async function handleLogin(e) {
e.preventDefault(); e.preventDefault();
@@ -221,7 +221,7 @@ async function handleLogin(e) {
await new Promise(resolve => setTimeout(resolve, 300)); await new Promise(resolve => setTimeout(resolve, 300));
try { try {
// Préparer les credentials pour SignalR // Preparer les credentials
const credentials = { const credentials = {
email: accessCode, // Utiliser directement le code agent comme email email: accessCode, // Utiliser directement le code agent comme email
password: password, password: password,
@@ -229,7 +229,7 @@ async function handleLogin(e) {
forceDisconnect: forceDisconnect // Ajouter l'option de déconnexion forcé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); const result = await ipcRenderer.invoke('login-agent', credentials);
if (result.success) { if (result.success) {
@@ -835,7 +835,7 @@ async function loadTerminals() {
console.log('Chargement des terminaux...'); console.log('Chargement des terminaux...');
try { try {
// Récupérer les terminaux depuis le serveur SignalR // Recuperer les terminaux depuis le serveur
const terminals = await ipcRenderer.invoke('get-terminal-list'); const terminals = await ipcRenderer.invoke('get-terminal-list');
availableTerminals = terminals || []; availableTerminals = terminals || [];
console.log(`${terminals.length} terminaux récupérés`); console.log(`${terminals.length} terminaux récupérés`);
@@ -1235,20 +1235,20 @@ function refreshCurrentWebview() {
} }
} }
// === GESTION INDICATEUR SIGNALR === // === GESTION INDICATEUR STATUT SERVEUR ===
function updateSignalRIndicator(status) { function updateServerIndicator(status) {
const indicator = document.getElementById('signalrIndicator'); const indicator = document.getElementById('signalrIndicator');
const text = document.getElementById('signalrText'); const text = document.getElementById('signalrText');
if (!indicator || !text) return; if (!indicator || !text) return;
// Réinitialiser les classes // Reinitialiser les classes
indicator.className = 'signalr-indicator'; indicator.className = 'signalr-indicator';
switch(status) { switch(status) {
case 'connected': case 'connected':
indicator.classList.add('connected'); indicator.classList.add('connected');
text.textContent = 'Connecté au serveur'; text.textContent = 'Serveur connecte';
break; break;
case 'connecting': case 'connecting':
indicator.classList.add('connecting'); indicator.classList.add('connecting');
@@ -1256,17 +1256,17 @@ function updateSignalRIndicator(status) {
break; break;
case 'disconnected': case 'disconnected':
indicator.classList.add('disconnected'); indicator.classList.add('disconnected');
text.textContent = 'Serveur déconnecté'; text.textContent = 'Serveur deconnecte';
break; break;
case 'error': case 'error':
indicator.classList.add('error'); indicator.classList.add('error');
text.textContent = 'Erreur de connexion'; text.textContent = 'Serveur injoignable';
break; break;
case 'disabled': case 'disabled':
indicator.classList.add('disabled'); indicator.classList.add('disabled');
text.textContent = 'SignalR désactivé'; text.textContent = 'Non configure';
break; break;
default: default:
text.textContent = 'État inconnu'; text.textContent = 'Etat inconnu';
} }
} }

151
socketio-adapter.js Normal file
View File

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