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