Files
SimpleClient-releases/main.js
Pierre Marx 77a310976b 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
2026-03-18 17:31:30 -04:00

616 lines
16 KiB
JavaScript

const { app, BrowserWindow, ipcMain, session, net } = require('electron');
const path = require('path');
const fs = require('fs');
const os = require('os');
const SocketIOAdapter = require('./socketio-adapter');
let mainWindow;
let config;
let currentAgent = null;
let currentTerminal = null;
let adapter = null;
let serverStatus = 'disconnected'; // disconnected, connecting, connected, error
let agentConnectionInfo = null;
let healthCheckInterval = null;
// Configuration du systeme de logs
const LOG_DIR = path.join(os.homedir(), '.simpleconnect-ng');
const LOG_FILE = path.join(LOG_DIR, 'socketio.log');
function ensureLogDirectory() {
if (!fs.existsSync(LOG_DIR)) {
fs.mkdirSync(LOG_DIR, { recursive: true });
}
}
function logToFile(message, data = null) {
ensureLogDirectory();
const timestamp = new Date().toISOString();
let logEntry = `[${timestamp}] ${message}`;
if (data) {
logEntry += '\n' + JSON.stringify(data, null, 2);
}
logEntry += '\n' + '-'.repeat(80) + '\n';
fs.appendFileSync(LOG_FILE, logEntry, 'utf8');
}
function log(message, data = null) {
console.log(message, data || '');
logToFile(message, data);
}
// Charger la configuration
function loadConfig() {
const configPath = path.join(__dirname, 'config.json');
const configData = fs.readFileSync(configPath, 'utf8');
config = JSON.parse(configData);
}
// Creer la fenetre principale
function createWindow() {
mainWindow = new BrowserWindow({
width: 1400,
height: 900,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
webviewTag: true,
webSecurity: false
},
icon: path.join(__dirname, 'icon.png'),
title: `SimpleConnect v${app.getVersion()}`,
autoHideMenuBar: true
});
mainWindow.setMenuBarVisibility(false);
mainWindow.loadFile('index.html');
mainWindow.webContents.on('did-finish-load', () => {
mainWindow.setTitle(`SimpleConnect v${app.getVersion()}`);
});
if (process.env.NODE_ENV === 'development') {
mainWindow.webContents.openDevTools();
}
mainWindow.on('closed', () => {
mainWindow = null;
});
}
// === GESTION SOCKET.IO ===
function initializeSocketIO() {
if (!config.socketio || !config.socketio.serverUrl) {
console.log('Socket.IO non configure');
serverStatus = 'disabled';
sendServerStatus();
return;
}
adapter = new SocketIOAdapter(config.socketio.serverUrl);
// Demarrer le health check polling
startHealthCheck();
}
// Health check periodique via GET /health
function startHealthCheck() {
const checkHealth = () => {
const serverUrl = config.socketio.serverUrl;
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();
}
});
request.on('error', () => {
serverStatus = 'error';
sendServerStatus();
});
request.end();
};
// Check immediat puis toutes les 10s
checkHealth();
healthCheckInterval = setInterval(checkHealth, 10000);
}
function sendServerStatus() {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('server-status', serverStatus);
}
}
// Gerer un appel entrant
function handleCallPickedUp(event) {
if (!mainWindow || !agentConnectionInfo) return;
const centres = processApplicationUrls(agentConnectionInfo.connList);
const centre = centres.find(c => c.queueName === event.queueName);
if (centre) {
console.log('Basculement vers le centre:', centre.nom);
mainWindow.webContents.send('switch-to-center', {
centreId: centre.id,
centreName: centre.nom,
queueName: event.queueName,
terminal: event.terminal,
eventType: 'call_pickup'
});
} else {
console.warn('Aucun centre trouve pour la file:', event.queueName);
}
}
// Gerer la fin d'un appel
function handleCallHungUp(event) {
if (!mainWindow) return;
console.log('Fin d\'appel sur la file:', event.queueName);
mainWindow.webContents.send('release-center', {
queueName: event.queueName,
terminal: event.terminal,
eventType: 'call_hangup'
});
}
// Configurer les handlers d'evenements Socket.IO apres connexion
function setupEventHandlers() {
if (!adapter) return;
log('Session Socket.IO demarree', {
serverUrl: config.socketio.serverUrl,
serviceProvider: config.socketio.serviceProvider
});
// Ecouter les evenements d'appels IPBX
adapter.on('ipbx_event', (data) => {
log('ipbx_event recu', data);
if (!agentConnectionInfo) return;
// Verifier que l'evenement est pour notre terminal
if (data.terminal !== currentTerminal) {
console.log('Evenement ignore - terminal different:', data.terminal, '!==', currentTerminal);
return;
}
switch (data.eventCode) {
case 1:
handleCallPickedUp(data);
break;
case 2:
handleCallHungUp(data);
break;
default:
log('Code evenement non gere:', data);
}
});
}
// Initialisation de l'application
app.whenReady().then(() => {
const { Menu } = require('electron');
Menu.setApplicationMenu(null);
session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
details.requestHeaders['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
callback({ requestHeaders: details.requestHeaders });
});
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': ['default-src * \'unsafe-inline\' \'unsafe-eval\' data: blob:;']
}
});
});
loadConfig();
createWindow();
initializeSocketIO();
});
// Quitter quand toutes les fenetres sont fermees
app.on('window-all-closed', async () => {
if (healthCheckInterval) {
clearInterval(healthCheckInterval);
}
// Deconnexion propre avant de quitter
if (currentAgent && adapter) {
try {
await adapter.logoff();
console.log('Agent deconnecte avant fermeture');
} catch (error) {
console.error('Erreur deconnexion:', error);
}
}
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (mainWindow === null) {
createWindow();
}
});
// === IPC HANDLERS ===
ipcMain.handle('get-config', () => {
return config;
});
ipcMain.handle('get-app-version', () => {
return app.getVersion();
});
// Renomme : get-signalr-status -> get-server-status
ipcMain.handle('get-server-status', () => {
return serverStatus;
});
// Recuperer la liste des terminaux via REST
ipcMain.handle('get-terminal-list', async () => {
if (!config.socketio || !config.socketio.serverUrl) {
return [];
}
try {
const provider = config.socketio.serviceProvider;
const url = `${config.socketio.serverUrl}/terminals?provider=${encodeURIComponent(provider)}`;
console.log('Recuperation des terminaux:', url);
return new Promise((resolve, reject) => {
const request = net.request(url);
let body = '';
request.on('response', (response) => {
response.on('data', (chunk) => {
body += chunk.toString();
});
response.on('end', () => {
if (response.statusCode === 200) {
try {
const terminals = JSON.parse(body);
log('Terminaux recuperes', { count: terminals.length, terminals });
resolve(Array.isArray(terminals) ? terminals : []);
} catch (e) {
console.error('Erreur parsing terminaux:', e);
resolve([]);
}
} else {
console.error('Erreur terminaux HTTP', response.statusCode);
resolve([]);
}
});
});
request.on('error', (error) => {
console.error('Erreur recuperation terminaux:', error);
resolve([]);
});
request.end();
});
} catch (error) {
console.error('Erreur recuperation terminaux:', error);
return [];
}
});
// Connexion agent via Socket.IO
ipcMain.handle('login-agent', async (event, credentials) => {
if (!adapter) {
return {
success: false,
message: 'Socket.IO non initialise. Veuillez reessayer.'
};
}
try {
console.log('Tentative de connexion agent:', credentials.email, 'Terminal:', credentials.terminal);
log('Tentative de connexion agent', {
email: credentials.email,
terminal: credentials.terminal,
forceDisconnect: credentials.forceDisconnect || false
});
// Deconnecter l'adapter precedent s'il y en a un
if (adapter.state === 'connected') {
adapter.disconnect();
}
// Recreer l'adapter pour une connexion fraiche
adapter = new SocketIOAdapter(config.socketio.serverUrl);
// Configurer les handlers d'evenements AVANT connect
setupEventHandlers();
// Connexion avec auth au handshake
const result = await adapter.connect(
credentials.email,
credentials.password,
credentials.terminal
);
if (result) {
console.log('Connexion reussie:', result);
log('Connexion agent reussie', {
accessCode: result.accessCode,
firstName: result.firstName,
lastName: result.lastName,
terminal: credentials.terminal,
connListCount: result.connList ? result.connList.length : 0,
connList: result.connList
});
agentConnectionInfo = result;
currentTerminal = credentials.terminal;
currentAgent = {
id: result.accessCode,
accessCode: result.accessCode,
name: `${result.firstName} ${result.lastName}`,
email: credentials.email,
firstName: result.firstName,
lastName: result.lastName,
terminal: credentials.terminal
};
const centres = processApplicationUrls(result.connList);
if (mainWindow) {
mainWindow.setTitle(
`SimpleConnect v${app.getVersion()} - Agent: ${currentAgent.accessCode} (${result.firstName} ${result.lastName}) - Tel: ${credentials.terminal}`
);
}
serverStatus = 'connected';
sendServerStatus();
return {
success: true,
agent: currentAgent,
centres: centres
};
}
return { success: false, message: 'Echec de l\'authentification' };
} catch (error) {
console.error('Erreur lors de la connexion agent:', error);
return {
success: false,
message: error.message || 'Erreur de connexion au serveur'
};
}
});
// Traiter les URLs des applications avec les placeholders
function processApplicationUrls(connList) {
if (!connList || connList.length === 0) return [];
return connList.map((conn, index) => {
let url = conn.applicationName;
// Remplacer les placeholders
if (url.includes('#CA#')) {
url = url.replace('#CA#', conn.accessCode || '');
}
if (url.includes('#MP#')) {
url = url.replace('#MP#', conn.password || '');
}
// Gerer les cas specifiques des plateformes connues
if (url === 'pro.mondocteur.fr' || url.includes('mondocteur.fr')) {
if (!url.startsWith('http')) {
url = 'https://pro.mondocteur.fr/backoffice.do';
}
} else if (url === 'pro.doctolib.fr' || url.includes('doctolib.fr')) {
if (!url.startsWith('http')) {
url = 'https://pro.doctolib.fr/signin';
}
} else if (!url.startsWith('http')) {
url = `https://${url}`;
}
return {
id: conn.code || `centre${index + 1}`,
nom: conn.queueName || conn.code || `Centre ${index + 1}`,
url: url,
couleur: getColorForIndex(index),
credentials: {
username: conn.accessCode,
password: conn.password
},
queueName: conn.queueName
};
});
}
function getColorForIndex(index) {
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#DDA0DD', '#FFD93D', '#6BCB77'];
return colors[index % colors.length];
}
// Deconnexion agent via Socket.IO
ipcMain.handle('logout', async () => {
if (currentAgent && adapter) {
try {
log('Logoff agent', {
accessCode: currentAgent.accessCode
});
await adapter.logoff();
console.log('Agent deconnecte du serveur');
log('Agent deconnecte avec succes', {
accessCode: currentAgent.accessCode,
name: currentAgent.name
});
} catch (error) {
console.error('Erreur lors de la deconnexion:', error);
}
}
currentAgent = null;
currentTerminal = null;
agentConnectionInfo = null;
if (mainWindow) {
mainWindow.setTitle(`SimpleConnect v${app.getVersion()}`);
}
return { success: true };
});
ipcMain.handle('quit-app', async () => {
app.quit();
});
ipcMain.handle('get-current-agent', () => {
if (!currentAgent || !agentConnectionInfo) return null;
const centres = processApplicationUrls(agentConnectionInfo.connList);
return {
agent: currentAgent,
centres: centres,
terminal: currentTerminal
};
});
ipcMain.handle('simulate-call', (event, callData) => {
mainWindow.webContents.send('incoming-call', callData);
return { success: true };
});
ipcMain.handle('get-simulated-calls', () => {
if (!config.cti || !config.cti.appelSimules) return [];
return config.cti.appelSimules.map(appel => {
const centre = config.centres ? config.centres.find(c => c.id === appel.centreId) : null;
return {
...appel,
centreNom: centre ? centre.nom : 'Centre inconnu'
};
});
});
// Sauvegarder les notes de l'agent
ipcMain.handle('save-notes', (event, noteData) => {
const notesDir = path.join(__dirname, 'notes');
if (!fs.existsSync(notesDir)) {
fs.mkdirSync(notesDir);
}
const fileName = `notes_${currentAgent.id}.json`;
const filePath = path.join(notesDir, fileName);
let notesData = {
agent: currentAgent.id,
agentName: currentAgent.name,
currentNote: noteData.content,
lastModified: new Date().toISOString(),
centre: noteData.centre,
history: []
};
if (fs.existsSync(filePath)) {
try {
const existingData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
if (existingData.currentNote && existingData.currentNote !== noteData.content) {
notesData.history = existingData.history || [];
notesData.history.unshift({
content: existingData.currentNote,
date: existingData.lastModified,
centre: existingData.centre
});
notesData.history = notesData.history.slice(0, 50);
}
} catch (error) {
console.error('Erreur lecture notes existantes:', error);
}
}
fs.writeFileSync(filePath, JSON.stringify(notesData, null, 2));
return { success: true, file: fileName };
});
ipcMain.handle('get-notes', () => {
if (!currentAgent) return null;
const notesDir = path.join(__dirname, 'notes');
const fileName = `notes_${currentAgent.id}.json`;
const filePath = path.join(notesDir, fileName);
if (fs.existsSync(filePath)) {
try {
const data = fs.readFileSync(filePath, 'utf8');
return JSON.parse(data);
} catch (error) {
console.error('Erreur lecture notes:', error);
return null;
}
}
return null;
});
ipcMain.handle('get-call-history', () => {
const historyFile = path.join(__dirname, 'call_history.json');
if (fs.existsSync(historyFile)) {
const data = fs.readFileSync(historyFile, 'utf8');
return JSON.parse(data);
}
return [];
});
ipcMain.handle('save-call-history', (event, callData) => {
const historyFile = path.join(__dirname, 'call_history.json');
let history = [];
if (fs.existsSync(historyFile)) {
const data = fs.readFileSync(historyFile, 'utf8');
history = JSON.parse(data);
}
history.unshift({
...callData,
agentId: currentAgent?.id,
timestamp: new Date().toISOString()
});
history = history.slice(0, 100);
fs.writeFileSync(historyFile, JSON.stringify(history, null, 2));
return { success: true };
});
ipcMain.handle('is-development', () => {
return process.env.NODE_ENV === 'development';
});