Files
SimpleClient-releases/main.js
Pierre Marx 7b9679e4dc feat: Affichage de la version dans l'interface et la barre de titre
- Ajout de l'affichage de la version à côté du logo dans l'interface
- Ajout de la version dans le titre de la fenêtre (barre macOS/Windows/Linux)
- Création du handler IPC get-app-version pour exposer la version
- Mise à jour dynamique du titre lors de la connexion/déconnexion agent
- Style élégant pour la version affichée dans l'interface (gris clair, opacité 0.8)

Fichiers modifiés :
- index.html : ajout du span pour la version
- renderer.js : récupération et affichage de la version via IPC
- main.js : handler IPC et mise à jour des titres de fenêtre
- styles-modern.css : style pour .app-version
2025-10-21 11:30:27 -04:00

791 lines
26 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const { app, BrowserWindow, ipcMain, session } = require('electron');
const path = require('path');
const fs = require('fs');
const os = require('os');
const signalR = require('@microsoft/signalr');
const ConnectionManager = require('./connection-manager');
let mainWindow;
let config;
let currentAgent = null;
let currentTerminal = null; // Terminal téléphonique de l'agent connecté
let signalRConnection = null;
let signalRStatus = 'disconnected'; // disconnected, connecting, connected, error
let agentConnectionInfo = null; // Informations complètes retournées par SignalR
// Configuration du système de logs SignalR
const SIGNALR_LOG_DIR = path.join(os.homedir(), '.simpleconnect-ng');
const SIGNALR_LOG_FILE = path.join(SIGNALR_LOG_DIR, 'signalr.log');
// Créer le répertoire de logs s'il n'existe pas
function ensureLogDirectory() {
if (!fs.existsSync(SIGNALR_LOG_DIR)) {
fs.mkdirSync(SIGNALR_LOG_DIR, { recursive: true });
console.log(`📁 Répertoire de logs créé: ${SIGNALR_LOG_DIR}`);
}
}
// Fonction pour écrire dans le fichier de log SignalR
function logToSignalRFile(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';
// Ajouter au fichier (append)
fs.appendFileSync(SIGNALR_LOG_FILE, logEntry, 'utf8');
}
// Logger dans la console ET dans le fichier
function logSignalR(message, data = null) {
console.log(message, data || '');
logToSignalRFile(message, data);
}
// Charger la configuration
function loadConfig() {
const configPath = path.join(__dirname, 'config.json');
const configData = fs.readFileSync(configPath, 'utf8');
config = JSON.parse(configData);
}
// Créer la fenêtre 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 // Cache la barre de menu par défaut
});
// Supprimer complètement le menu (pour Linux/Windows)
mainWindow.setMenuBarVisibility(false);
// Charger l'interface HTML
mainWindow.loadFile('index.html');
// Ouvrir les DevTools uniquement en mode développement
if (process.env.NODE_ENV === 'development') {
mainWindow.webContents.openDevTools();
}
// Gérer la fermeture de la fenêtre
mainWindow.on('closed', () => {
mainWindow = null;
});
}
// === GESTION SIGNALR/WEBSOCKET ===
function initializeSignalR() {
if (!config.signalR || !config.signalR.enabled) {
console.log('SignalR/WebSocket désactivé dans la configuration');
signalRStatus = 'disabled';
sendSignalRStatus();
return;
}
try {
// 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
signalRConnection = connectionManager;
// 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() {
// Configuration des handlers après la connexion
const connection = signalRConnection.getConnection();
if (!connection) {
console.error('Pas de connexion active pour configurer les handlers');
return;
}
// === 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('═══════════════════════════════════════════════════════════');
// 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
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 handleCallPickedUp(event) {
if (!mainWindow || !agentConnectionInfo) return;
// Identifier le centre correspondant à la file
const centres = processApplicationUrls(agentConnectionInfo.connList);
const centre = centres.find(c => c.queueName === event.queueName);
if (centre) {
console.log('Basculement vers le centre:', centre.nom);
// Envoyer l'instruction de basculement à la fenêtre
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 trouvé pour la file:', event.queueName);
}
}
// Gérer la fin d'un appel
function handleCallHungUp(event) {
if (!mainWindow) return;
console.log('Fin d\'appel sur la file:', event.queueName);
// Envoyer l'instruction de libération à la fenêtre
mainWindow.webContents.send('release-center', {
queueName: event.queueName,
terminal: event.terminal,
eventType: 'call_hangup'
});
}
async function startSignalRConnection() {
try {
signalRStatus = 'connecting';
sendSignalRStatus();
logSignalR('🔌 Tentative de connexion au serveur...', {
serverUrl: config.ServerIp || config.signalR.serverUrl,
status: 'connecting'
});
// Le ConnectionManager gère le fallback automatiquement
const connection = await signalRConnection.connect();
// Déterminer quel type de connexion a réussi
const connectionInfo = signalRConnection.getConnectionInfo();
console.log(`Connexion établie via ${connectionInfo.type}`);
logSignalR(`✅ Connexion établie avec succès (${connectionInfo.type})`, {
connectionType: connectionInfo.type,
isWebSocketFallback: connectionInfo.isWebSocketFallback,
status: 'connected',
serverUrl: connectionInfo.serverUrl
});
// 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
app.whenReady().then(() => {
// Supprimer le menu de l'application complètement (toutes plateformes)
const { Menu } = require('electron');
Menu.setApplicationMenu(null);
// Configuration de la session pour éviter les problèmes CORS
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();
// Initialiser SignalR après le chargement de la config
initializeSignalR();
});
// Quitter quand toutes les fenêtres sont fermées
app.on('window-all-closed', async () => {
// Déconnexion propre avant de quitter
if (currentAgent && signalRConnection && signalRStatus === 'connected') {
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
// Le serveur .NET ne supporte pas ce message
// On laisse la connexion se fermer naturellement avec l'application
if (process.platform !== 'darwin') {
app.quit();
}
});
// Réactiver l'app sur macOS
app.on('activate', () => {
if (mainWindow === null) {
createWindow();
}
});
// === IPC HANDLERS ===
// Obtenir la configuration
ipcMain.handle('get-config', () => {
return config;
});
// Obtenir la version de l'application
ipcMain.handle('get-app-version', () => {
return app.getVersion();
});
// Obtenir le statut SignalR
ipcMain.handle('get-signalr-status', () => {
return signalRStatus;
});
// Récupérer la liste des terminaux téléphoniques
ipcMain.handle('get-terminal-list', async () => {
// Mode simulation si SignalR non connecté
if (!signalRConnection || signalRStatus !== 'connected') {
console.log('SignalR non connecté, utilisation des terminaux de simulation');
return config.signalR.terminalsSimulation || ['3001', '3002', '3003'];
}
try {
console.log('Récupération des terminaux pour:', config.signalR.serviceProvider);
logSignalR('📡 Invocation SignalR: GetTerminalListByServiceProvider', {
method: 'GetTerminalListByServiceProvider',
serviceProvider: config.signalR.serviceProvider
});
const terminals = await signalRConnection.invoke(
'GetTerminalListByServiceProvider',
config.signalR.serviceProvider
);
console.log('Terminaux disponibles:', terminals);
logSignalR('📞 Terminaux récupérés', {
count: terminals.length,
terminals: terminals
});
return terminals || [];
} catch (error) {
console.error('Erreur récupération terminaux:', error);
// Retourner les terminaux de simulation en cas d'erreur
return config.signalR.terminalsSimulation || ['3001', '3002', '3003'];
}
});
// Connexion agent via SignalR
ipcMain.handle('login-agent', async (event, credentials) => {
// Vérifier que SignalR est connecté
if (!signalRConnection || signalRStatus !== 'connected') {
return {
success: false,
message: 'Connexion au serveur SignalR non établie. Veuillez réessayer.'
};
}
try {
console.log('Tentative de connexion agent:', credentials.email, 'Terminal:', credentials.terminal);
logSignalR('🔐 Tentative de connexion agent', {
email: credentials.email,
terminal: credentials.terminal,
forceDisconnect: credentials.forceDisconnect || false
});
// Si déconnexion forcée demandée, déconnecter d'abord la session précédente
if (credentials.forceDisconnect) {
console.log('Déconnexion forcée demandée pour:', credentials.email);
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
logSignalR('📡 Invocation SignalR: AgentLogin', {
method: 'AgentLogin',
email: credentials.email,
terminal: credentials.terminal
});
const result = await signalRConnection.invoke('AgentLogin',
credentials.email,
credentials.password,
credentials.terminal
);
if (result) {
console.log('Connexion réussie:', result);
logSignalR('✅ 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
});
// Stocker les informations de connexion
agentConnectionInfo = result;
currentTerminal = credentials.terminal;
// Créer l'objet agent pour compatibilité
currentAgent = {
id: result.accessCode,
accessCode: result.accessCode,
name: `${result.firstName} ${result.lastName}`,
email: credentials.email,
firstName: result.firstName,
lastName: result.lastName,
terminal: credentials.terminal
};
// Traiter les URLs des applications et créer les centres
const centres = processApplicationUrls(result.connList);
// Mettre à jour le titre de la fenêtre
if (mainWindow) {
mainWindow.setTitle(
`SimpleConnect v${app.getVersion()} - Agent: ${currentAgent.accessCode} (${result.firstName} ${result.lastName}) - Tel: ${credentials.terminal}`
);
}
return {
success: true,
agent: currentAgent,
centres: centres
};
}
return { success: false, message: 'Échec 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 || '');
}
// Gérer les cas spécifiques 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')) {
// Ajouter https:// par défaut si pas de protocole
url = `https://${url}`;
}
// Créer l'objet centre compatible avec l'interface
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 // Garder pour le mapping avec les événements IPBX
};
});
}
// Fonction helper pour attribuer des couleurs aux centres
function getColorForIndex(index) {
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#DDA0DD', '#FFD93D', '#6BCB77'];
return colors[index % colors.length];
}
// Déconnexion agent via SignalR
ipcMain.handle('logout', async () => {
if (currentAgent && signalRConnection && signalRStatus === 'connected') {
try {
// Appeler SignalR pour la déconnexion
logSignalR('📡 Invocation SignalR: AgentLogoff', {
method: 'AgentLogoff',
accessCode: currentAgent.accessCode
});
await signalRConnection.invoke('AgentLogoff', currentAgent.accessCode);
console.log('Agent déconnecté du serveur SignalR');
logSignalR('👋 Agent déconnecté avec succès', {
accessCode: currentAgent.accessCode,
name: currentAgent.name
});
} catch (error) {
console.error('Erreur lors de la déconnexion SignalR:', error);
}
}
// Réinitialiser les variables locales
currentAgent = null;
currentTerminal = null;
agentConnectionInfo = null;
// Réinitialiser le titre de la fenêtre
if (mainWindow) {
mainWindow.setTitle(`SimpleConnect v${app.getVersion()}`);
}
return { success: true };
});
// Handler pour quitter l'application proprement
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();
});
// Obtenir l'agent actuel
ipcMain.handle('get-current-agent', () => {
if (!currentAgent || !agentConnectionInfo) return null;
// Retourner les centres traités depuis SignalR
const centres = processApplicationUrls(agentConnectionInfo.connList);
return {
agent: currentAgent,
centres: centres,
terminal: currentTerminal
};
});
// Simuler un appel entrant
ipcMain.handle('simulate-call', (event, callData) => {
// Envoyer l'événement d'appel entrant à la fenêtre
mainWindow.webContents.send('incoming-call', callData);
return { success: true };
});
// Obtenir les données pour simuler des appels
ipcMain.handle('get-simulated-calls', () => {
return config.cti.appelSimules.map(appel => {
const centre = config.centres.find(c => c.id === appel.centreId);
return {
...appel,
centreNom: centre ? centre.nom : 'Centre inconnu'
};
});
});
// Sauvegarder les notes de l'agent (un seul fichier par agent)
ipcMain.handle('save-notes', (event, noteData) => {
const notesDir = path.join(__dirname, 'notes');
if (!fs.existsSync(notesDir)) {
fs.mkdirSync(notesDir);
}
// Un seul fichier par agent, mis à jour à chaque sauvegarde
const fileName = `notes_${currentAgent.id}.json`;
const filePath = path.join(notesDir, fileName);
// Lire l'historique existant si le fichier existe
let notesData = {
agent: currentAgent.id,
agentName: currentAgent.name,
currentNote: noteData.content,
lastModified: new Date().toISOString(),
centre: noteData.centre,
history: []
};
// Si le fichier existe, préserver l'historique
if (fs.existsSync(filePath)) {
try {
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) {
notesData.history = existingData.history || [];
notesData.history.unshift({
content: existingData.currentNote,
date: existingData.lastModified,
centre: existingData.centre
});
// Limiter l'historique à 50 entrées
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 };
});
// Récupérer les notes de l'agent
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;
});
// Obtenir l'historique des appels
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 [];
});
// Sauvegarder un appel dans l'historique
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()
});
// Garder seulement les 100 derniers appels
history = history.slice(0, 100);
fs.writeFileSync(historyFile, JSON.stringify(history, null, 2));
return { success: true };
});
// Vérifier si on est en mode développement
ipcMain.handle('is-development', () => {
return process.env.NODE_ENV === 'development';
});