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:
@@ -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"]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
538
main.js
538
main.js
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// === LOGGER UNIVERSEL POUR TOUS LES MESSAGES SIGNALR ===
|
const request = net.request(`${serverUrl}/health`);
|
||||||
// Intercepter TOUS les messages reçus du serveur pour découvrir les événements disponibles
|
|
||||||
|
|
||||||
// Initialiser le fichier de log avec une session
|
request.on('response', (response) => {
|
||||||
logSignalR('════════════════════════════════════════════════════════════');
|
if (response.statusCode === 200) {
|
||||||
logSignalR('🚀 NOUVELLE SESSION SIGNALR DÉMARRÉE');
|
if (serverStatus !== 'connected') {
|
||||||
logSignalR(`Application: SimpleConnect v${app.getVersion()}`);
|
// Ne passer en 'connected' que si on n'a pas d'agent connecte
|
||||||
logSignalR(`Serveur SignalR: ${config.signalR.serverUrl}`);
|
// (sinon le status est deja 'connected' via le login)
|
||||||
logSignalR(`Service Provider: ${config.signalR.serviceProvider}`);
|
if (!currentAgent) {
|
||||||
logSignalR('════════════════════════════════════════════════════════════');
|
serverStatus = 'connected';
|
||||||
|
sendServerStatus();
|
||||||
// Liste des événements connus pour les logger différemment
|
}
|
||||||
const knownEvents = ['IpbxEvent'];
|
}
|
||||||
|
} else {
|
||||||
// Créer un proxy pour intercepter tous les appels .on()
|
serverStatus = 'error';
|
||||||
const originalOn = signalRConnection.on.bind(signalRConnection);
|
sendServerStatus();
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Fonction originale pour traiter les IpbxEvent
|
request.on('error', () => {
|
||||||
function handleIpbxEventOriginal(args) {
|
serverStatus = 'error';
|
||||||
if (!args || !agentConnectionInfo) return;
|
sendServerStatus();
|
||||||
|
|
||||||
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
|
request.end();
|
||||||
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
|
// Check immediat puis toutes les 10s
|
||||||
switch(event.eventCode) {
|
checkHealth();
|
||||||
case 0:
|
healthCheckInterval = setInterval(checkHealth, 10000);
|
||||||
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...', {
|
|
||||||
serverUrl: config.ServerIp || config.signalR.serverUrl,
|
|
||||||
status: 'connecting'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Le ConnectionManager gère le fallback automatiquement
|
log('Session Socket.IO demarree', {
|
||||||
const connection = await signalRConnection.connect();
|
serverUrl: config.socketio.serverUrl,
|
||||||
|
serviceProvider: config.socketio.serviceProvider
|
||||||
|
});
|
||||||
|
|
||||||
// Déterminer quel type de connexion a réussi
|
// Ecouter les evenements d'appels IPBX
|
||||||
const connectionInfo = signalRConnection.getConnectionInfo();
|
adapter.on('ipbx_event', (data) => {
|
||||||
|
log('ipbx_event recu', data);
|
||||||
|
|
||||||
console.log(`Connexion établie via ${connectionInfo.type}`);
|
if (!agentConnectionInfo) return;
|
||||||
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
|
// Verifier que l'evenement est pour notre terminal
|
||||||
setupSignalRHandlers();
|
if (data.terminal !== currentTerminal) {
|
||||||
|
console.log('Evenement ignore - terminal different:', data.terminal, '!==', currentTerminal);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
signalRStatus = 'connected';
|
switch (data.eventCode) {
|
||||||
sendSignalRStatus();
|
case 1:
|
||||||
|
handleCallPickedUp(data);
|
||||||
} catch (error) {
|
break;
|
||||||
console.error('Erreur connexion SignalR:', error);
|
case 2:
|
||||||
logSignalR('❌ Erreur de connexion SignalR', {
|
handleCallHungUp(data);
|
||||||
error: error.message,
|
break;
|
||||||
stack: error.stack,
|
default:
|
||||||
serverUrl: config.signalR.serverUrl
|
log('Code evenement non gere:', data);
|
||||||
});
|
}
|
||||||
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
|
|
||||||
});
|
|
||||||
|
|
||||||
const terminals = await signalRConnection.invoke(
|
return new Promise((resolve, reject) => {
|
||||||
'GetTerminalListByServiceProvider',
|
const request = net.request(url);
|
||||||
config.signalR.serviceProvider
|
let body = '';
|
||||||
);
|
|
||||||
|
|
||||||
console.log('Terminaux disponibles:', terminals);
|
request.on('response', (response) => {
|
||||||
logSignalR('📞 Terminaux récupérés', {
|
response.on('data', (chunk) => {
|
||||||
count: terminals.length,
|
body += chunk.toString();
|
||||||
terminals: terminals
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
});
|
});
|
||||||
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';
|
||||||
});
|
});
|
||||||
@@ -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": {
|
||||||
|
|||||||
42
renderer.js
42
renderer.js
@@ -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
151
socketio-adapter.js
Normal 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;
|
||||||
Reference in New Issue
Block a user