Files
PTT-Live/electron/main.js
T
benoit 22bb66b680 fix: corriger la détection de statut serveur et l'URL/QR code de connexion clients
Statut serveur :
- SERVER_URL utilisait "localhost", que le Node embarqué par Electron peut
  résoudre en IPv6 (::1) en priorité ; le serveur n'écoutant qu'en IPv4
  (host: 0.0.0.0), le ping de statut échouait silencieusement alors que le
  serveur tournait. Bascule sur 127.0.0.1 (main.js + preload.js).
- L'erreur réelle de pingServer() n'était jamais remontée au renderer
  (health.error perdu) ; elle est maintenant incluse dans la réponse IPC.

URL/QR code clients :
- L'URL affichée utilisait un remplacement de chaîne ("localhost" -> IP) qui
  ne matchait plus rien depuis le passage à 127.0.0.1 ; remplacé par un
  parsing d'URL qui ne réutilise que le protocole/port.
- Le QR code dépendait d'une lib chargée depuis un CDN externe, inadapté à
  une app self-hosted censée fonctionner sans accès Internet sur le WiFi
  d'un événement. Généré désormais côté Main Process avec la lib qrcode
  (déjà en dépendance, jamais utilisée) et transmis au renderer en data URL ;
  suppression du fichier placeholder et de la dépendance CDN.
- getNetworkIP() lisait /admin/config, qui renvoie la valeur YAML brute
  "AUTO" (jamais résolue), donc retombait toujours sur "localhost".
  Remplacé par la détection réseau du Main Process (même logique que pour
  les certificats mkcert).
- Ajout d'un placeholder visuel (icône + message) tant qu'aucun QR code
  n'est généré ou que le serveur est arrêté, en CSS pur.
2026-06-30 14:11:29 +02:00

464 lines
13 KiB
JavaScript

/**
* PTT Live Desktop - Main Process
* Intègre le serveur Node.js existant dans une application Electron
*/
const { app, BrowserWindow, ipcMain, Menu, Tray, dialog } = require('electron');
const path = require('path');
const { spawn } = require('child_process');
const http = require('http');
const https = require('https');
const QRCode = require('qrcode');
const setupHelper = require('./setup-helper');
// État de l'application
let mainWindow = null;
let tray = null;
let serverProcess = null;
let serverStarted = false;
let rendererReady = false;
const SERVER_PORT = process.env.PORT || 3000;
// HTTPS activé par défaut (cohérent avec le setup mkcert automatique au premier
// lancement) ; ENABLE_HTTPS=false permet de revenir explicitement en HTTP
const ENABLE_HTTPS = process.env.ENABLE_HTTPS !== 'false';
const SERVER_PROTOCOL = ENABLE_HTTPS ? 'https' : 'http';
// 127.0.0.1 plutôt que localhost : le serveur n'écoute qu'en IPv4 (host: 0.0.0.0
// dans config.yaml), or le Node embarqué par Electron peut résoudre "localhost"
// en IPv6 (::1) en priorité, ce qui ferait échouer silencieusement le ping
const SERVER_URL = `${SERVER_PROTOCOL}://127.0.0.1:${SERVER_PORT}`;
const isDev = process.argv.includes('--dev');
/**
* Créer la fenêtre principale
*/
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 900,
minHeight: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false,
contextIsolation: true
},
title: 'PTT Live Server',
backgroundColor: '#1a1a1a'
});
// Charger l'interface dashboard
mainWindow.loadFile(path.join(__dirname, 'ui', 'index.html'));
// Attendre que le renderer soit prêt
mainWindow.webContents.on('did-finish-load', () => {
rendererReady = true;
console.log('✅ Interface chargée');
// Envoyer l'état initial du serveur
if (mainWindow) {
mainWindow.webContents.send('server:status', { running: serverStarted });
}
});
// DevTools en mode dev
if (isDev) {
mainWindow.webContents.openDevTools();
}
// Cleanup à la fermeture
mainWindow.on('closed', () => {
mainWindow = null;
rendererReady = false;
});
}
/**
* Créer la tray icon (macOS/Linux)
*/
function createTray() {
// TODO: créer une vraie icône
// tray = new Tray(path.join(__dirname, 'assets', 'tray-icon.png'));
const contextMenu = Menu.buildFromTemplate([
{
label: 'Ouvrir Dashboard',
click: () => {
if (mainWindow) {
mainWindow.show();
} else {
createWindow();
}
}
},
{ type: 'separator' },
{
label: serverStarted ? '🟢 Serveur actif' : '⚪ Serveur arrêté',
enabled: false
},
{
label: serverStarted ? 'Arrêter serveur' : 'Démarrer serveur',
click: async () => {
if (serverStarted) {
await stopServer();
} else {
await startServer();
}
}
},
{ type: 'separator' },
{
label: 'Quitter',
click: () => {
app.quit();
}
}
]);
if (tray) {
tray.setContextMenu(contextMenu);
tray.setToolTip('PTT Live Server');
}
}
/**
* Démarrer le serveur Node.js
*/
async function startServer() {
return new Promise((resolve, reject) => {
if (serverProcess) {
console.log('⚠️ Serveur déjà démarré');
resolve({ success: false, message: 'Server already running' });
return;
}
console.log('🚀 Démarrage du serveur PTT Live...');
const serverPath = path.join(__dirname, '..', 'server', 'index.js');
serverProcess = spawn('node', [serverPath], {
cwd: path.join(__dirname, '..', 'server'),
env: {
...process.env,
PORT: SERVER_PORT,
USE_LOCAL_LIVEKIT: 'true',
ENABLE_HTTPS: ENABLE_HTTPS ? 'true' : 'false',
NODE_ENV: isDev ? 'development' : 'production'
}
});
serverProcess.stdout.on('data', (data) => {
const output = data.toString();
console.log('[Serveur]', output);
// Transmettre les logs au renderer (seulement si prêt)
if (mainWindow && rendererReady) {
mainWindow.webContents.send('server:log', {
level: 'info',
message: output.trim()
});
}
// Détecter démarrage réussi
if (output.includes('Serveur prêt') || output.includes('API REST démarrée')) {
serverStarted = true;
console.log('✅ Serveur démarré avec succès');
if (mainWindow && rendererReady) {
mainWindow.webContents.send('server:status', { running: true });
}
createTray(); // Mettre à jour tray
resolve({ success: true, url: SERVER_URL });
}
});
serverProcess.stderr.on('data', (data) => {
const output = data.toString();
// LiveKit envoie INFO/WARN dans stderr (comportement normal Go)
// Ne les traiter comme erreurs que s'ils contiennent vraiment "ERROR"
const isError = output.includes('ERROR') || output.includes('Error:');
console.log(isError ? '[Serveur Error]' : '[Serveur]', output);
if (mainWindow && rendererReady) {
mainWindow.webContents.send('server:log', {
level: isError ? 'error' : 'info',
message: output.trim()
});
}
// Détecter démarrage LiveKit dans stderr
if (output.includes('starting LiveKit server') || output.includes('Serveur prêt')) {
if (!serverStarted) {
serverStarted = true;
console.log('✅ Serveur démarré (détecté via stderr)');
if (mainWindow && rendererReady) {
mainWindow.webContents.send('server:status', { running: true });
}
createTray();
resolve({ success: true, url: SERVER_URL });
}
}
});
serverProcess.on('error', (error) => {
console.error('❌ Erreur démarrage serveur:', error);
serverStarted = false;
if (mainWindow && rendererReady) {
mainWindow.webContents.send('server:status', { running: false, error: error.message });
}
reject(error);
});
serverProcess.on('exit', (code, signal) => {
console.log(`⚠️ Serveur arrêté (code: ${code}, signal: ${signal})`);
serverProcess = null;
serverStarted = false;
if (mainWindow && rendererReady) {
mainWindow.webContents.send('server:status', { running: false });
}
createTray(); // Mettre à jour tray
});
// Timeout de sécurité (15s)
setTimeout(() => {
if (!serverStarted && serverProcess) {
console.log('⏱️ Timeout démarrage serveur (15s), vérification health...');
// Vérifier que le serveur répond vraiment
pingServer().then((health) => {
if (health.success) {
serverStarted = true;
console.log('✅ Serveur répond au health check');
if (mainWindow) {
mainWindow.webContents.send('server:status', { running: true });
}
createTray();
resolve({ success: true, url: SERVER_URL });
} else {
console.error('❌ Serveur ne répond pas après 15s');
reject(new Error('Server startup timeout'));
}
});
}
}, 15000);
});
}
/**
* Arrêter le serveur Node.js
*/
async function stopServer() {
return new Promise((resolve) => {
if (!serverProcess) {
console.log('⚠️ Aucun serveur à arrêter');
resolve({ success: false, message: 'No server running' });
return;
}
console.log('🛑 Arrêt du serveur...');
serverProcess.on('exit', () => {
serverProcess = null;
serverStarted = false;
console.log('✅ Serveur arrêté');
if (mainWindow) {
mainWindow.webContents.send('server:status', { running: false });
}
createTray();
resolve({ success: true });
});
// Envoyer SIGTERM (shutdown gracieux)
serverProcess.kill('SIGTERM');
// Forcer après 5s si nécessaire
setTimeout(() => {
if (serverProcess) {
console.log('⚠️ Force kill du serveur');
serverProcess.kill('SIGKILL');
}
}, 5000);
});
}
/**
* Tester si le serveur répond
*/
async function pingServer() {
return new Promise((resolve) => {
const client = ENABLE_HTTPS ? https : http;
// rejectUnauthorized: false : le cert mkcert est approuvé par le Keychain
// macOS (Safari/Chrome/Electron renderer), mais le module https de Node
// ne lit pas ce trust store et rejetterait sinon ce ping vers notre
// propre serveur local.
const options = ENABLE_HTTPS ? { rejectUnauthorized: false } : {};
client.get(`${SERVER_URL}/health`, options, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
try {
const json = JSON.parse(data);
resolve({ success: true, data: json });
} catch (e) {
resolve({ success: false, error: 'Invalid response' });
}
});
}).on('error', (err) => {
resolve({ success: false, error: err.message });
});
});
}
// ========== App Lifecycle ==========
app.whenReady().then(async () => {
// Setup IPC Handlers (doit être après app.whenReady)
ipcMain.handle('server:start', async () => {
return await startServer();
});
ipcMain.handle('server:stop', async () => {
return await stopServer();
});
ipcMain.handle('server:status', async () => {
if (!serverStarted) {
return { running: false };
}
const health = await pingServer();
return {
running: health.success,
health: health.data,
error: health.error,
url: SERVER_URL
};
});
ipcMain.handle('server:ping', async () => {
return await pingServer();
});
ipcMain.handle('qrcode:generate', async (event, text) => {
try {
const dataUrl = await QRCode.toDataURL(text, { width: 256, margin: 2 });
return { success: true, dataUrl };
} catch (error) {
return { success: false, error: error.message };
}
});
ipcMain.handle('network:ip', async () => {
return setupHelper.getNetworkIP();
});
// Créer fenêtre
createWindow();
createTray();
// Vérifier setup automatique (certificats)
console.log('🔍 Vérification configuration...');
const projectRoot = path.join(__dirname, '..');
const certsDir = path.join(projectRoot, 'certs');
if (!setupHelper.certificatesExist(certsDir)) {
console.log('⚠️ Certificats SSL manquants, configuration automatique...\n');
// Afficher dialog d'information
const infoResult = await dialog.showMessageBox(mainWindow, {
type: 'info',
title: 'Configuration initiale',
message: 'Première utilisation de PTT Live',
detail: 'Configuration des certificats SSL en cours...\nCela peut prendre 1-2 minutes.\n\nmkcert sera installé automatiquement.',
buttons: ['Continuer', 'Annuler']
});
if (infoResult.response === 1) {
console.log('⚠️ Configuration annulée par l\'utilisateur');
return;
}
// Lancer setup auto
const setupResult = await setupHelper.autoSetup(projectRoot);
if (!setupResult.success) {
// Échec du setup automatique
await dialog.showMessageBox(mainWindow, {
type: 'error',
title: 'Configuration échouée',
message: 'Impossible de configurer automatiquement les certificats SSL',
detail: setupResult.manual
? 'Veuillez exécuter manuellement :\n./setup-certificates.sh\n\nOu installer mkcert : https://github.com/FiloSottile/mkcert'
: setupResult.error,
buttons: ['OK']
});
console.error('❌ Setup automatique échoué');
return; // Ne pas démarrer le serveur
}
// Setup réussi
await dialog.showMessageBox(mainWindow, {
type: 'info',
title: 'Configuration terminée',
message: 'Certificats SSL configurés avec succès !',
detail: `Votre IP réseau : ${setupResult.networkIP}\n\nLe serveur va démarrer...`,
buttons: ['OK']
});
console.log('✅ Setup automatique terminé\n');
} else {
console.log('✅ Certificats présents\n');
}
// NE PAS démarrer automatiquement
// L'utilisateur cliquera sur "Démarrer" dans l'interface
console.log('✅ Application prête');
console.log('💡 Cliquez sur "Démarrer" pour lancer le serveur\n');
});
app.on('window-all-closed', () => {
// Ne pas quitter l'app sur macOS (comportement standard)
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// Cleanup au quit
app.on('before-quit', async (event) => {
if (serverProcess) {
event.preventDefault();
console.log('🧹 Cleanup avant fermeture...');
await stopServer();
app.quit();
}
});
// Gestion des erreurs non catchées
process.on('uncaughtException', (error) => {
console.error('❌ Erreur non catchée:', error);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('❌ Promise rejection non gérée:', reason);
});