feat: logs structurés avec couleurs ANSI et format lisible

- Banner au démarrage avec version et URL serveur
- Pastilles ✓/✗ pour connexion/déconnexion serveur
- Terminaux récupérés fusionnés dans la ligne de connexion
- Timestamps hh:mm:ss en console, date complète dans le fichier log
- Strip ANSI dans le fichier log pour lisibilité SSH
- Log de fermeture avant app.quit()
- Terminaux chargés uniquement quand le serveur est joignable
This commit is contained in:
Pierre Marx
2026-03-18 20:25:13 -04:00
parent 42be82ed8c
commit f49e4bb1e1

156
main.js
View File

@@ -23,26 +23,60 @@ function ensureLogDirectory() {
}
}
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');
}
const timestamp = new Date().toISOString();
let logEntry = `[${timestamp}] ${message}`;
function logSectionToFile(message) {
ensureLogDirectory();
fs.appendFileSync(LOG_FILE, `[${fileTimestamp()}] ── ${stripAnsi(message)} ──\n`, 'utf8');
}
if (data) {
logEntry += '\n' + JSON.stringify(data, null, 2);
}
// ANSI colors
const c = {
reset: '\x1b[0m',
dim: '\x1b[2m',
green: '\x1b[32m',
red: '\x1b[31m',
cyan: '\x1b[36m',
bold: '\x1b[1m',
};
logEntry += '\n' + '-'.repeat(80) + '\n';
fs.appendFileSync(LOG_FILE, logEntry, 'utf8');
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(message, data || '');
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');
@@ -92,49 +126,77 @@ function initializeSocketIO() {
return;
}
logBanner(app.getVersion(), config.socketio.serverUrl);
adapter = new SocketIOAdapter(config.socketio.serverUrl);
// Demarrer le health check polling
// 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 (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';
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 10s
// Check immediat puis toutes les 5s
checkHealth();
healthCheckInterval = setInterval(checkHealth, 10000);
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);
}
@@ -177,7 +239,7 @@ function handleCallHungUp(event) {
function setupEventHandlers() {
if (!adapter) return;
log('Session Socket.IO demarree', {
log('Session Socket.IO démarrée', {
serverUrl: config.socketio.serverUrl,
serviceProvider: config.socketio.serviceProvider
});
@@ -237,17 +299,18 @@ app.on('window-all-closed', async () => {
clearInterval(healthCheckInterval);
}
// Deconnexion propre avant de quitter
// Déconnexion propre avant de quitter
if (currentAgent && adapter) {
try {
await adapter.logoff();
console.log('Agent deconnecte avant fermeture');
} catch (error) {
console.error('Erreur deconnexion:', error);
console.error('Erreur déconnexion:', error);
}
}
if (process.platform !== 'darwin') {
console.log(`${c.dim}${formatTimestamp()}${c.reset} ${c.dim}Application fermée${c.reset}`);
logSectionToFile('Application fermée');
app.quit();
}
});
@@ -282,8 +345,6 @@ ipcMain.handle('get-terminal-list', async () => {
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 = '';
@@ -297,7 +358,7 @@ ipcMain.handle('get-terminal-list', async () => {
if (response.statusCode === 200) {
try {
const terminals = JSON.parse(body);
log('Terminaux recuperes', { count: terminals.length, terminals });
log(`${c.green}${c.reset} Serveur connecté — ${terminals.length} terminaux`);
resolve(Array.isArray(terminals) ? terminals : []);
} catch (e) {
console.error('Erreur parsing terminaux:', e);
@@ -348,6 +409,26 @@ ipcMain.handle('login-agent', async (event, credentials) => {
// 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();
@@ -359,8 +440,8 @@ ipcMain.handle('login-agent', async (event, credentials) => {
);
if (result) {
console.log('Connexion reussie:', result);
log('Connexion agent reussie', {
console.log('Connexion réussie:', result);
log('Connexion agent réussie', {
accessCode: result.accessCode,
firstName: result.firstName,
lastName: result.lastName,
@@ -390,6 +471,8 @@ ipcMain.handle('login-agent', async (event, credentials) => {
);
}
// Agent connecte → l'adapter pilote le voyant, on arrete le health check
stopHealthCheck();
serverStatus = 'connected';
sendServerStatus();
@@ -466,8 +549,8 @@ ipcMain.handle('logout', async () => {
accessCode: currentAgent.accessCode
});
await adapter.logoff();
console.log('Agent deconnecte du serveur');
log('Agent deconnecte avec succes', {
console.log('Agent déconnecté du serveur');
log('Agent déconnecté avec succès', {
accessCode: currentAgent.accessCode,
name: currentAgent.name
});
@@ -480,6 +563,9 @@ ipcMain.handle('logout', async () => {
currentTerminal = null;
agentConnectionInfo = null;
// Retour ecran login → relancer le health check
startHealthCheck();
if (mainWindow) {
mainWindow.setTitle(`SimpleConnect v${app.getVersion()}`);
}
@@ -488,6 +574,8 @@ ipcMain.handle('logout', async () => {
});
ipcMain.handle('quit-app', async () => {
console.log(`${c.dim}${formatTimestamp()}${c.reset} ${c.dim}Application fermée${c.reset}`);
logSectionToFile('Application fermée');
app.quit();
});