530c3a10b2
- Main Process spawn serveur automatiquement avec IPC sécurisé - Dashboard temps réel : stats, utilisateurs, QR Code - Configuration audio : devices, sample rate, bitrate, jitter buffer - Gestion groupes : CRUD complet via API admin - Monitoring : logs temps réel filtrables par niveau - Notifications : toast visuelles avec auto-dismiss - Packaging : electron-builder pour macOS (.dmg) et Linux (.deb/.AppImage) - Documentation : README technique, QUICKSTART, CHANGELOG, guide utilisateur Structure : - electron/main.js (333 lignes) : Main Process + spawn serveur - electron/preload.js (31 lignes) : IPC bridge sécurisé - electron/ui/index.html (187 lignes) : interface dashboard - electron/ui/styles.css (556 lignes) : dark theme - electron/ui/app.js (626 lignes) : logic frontend Total : 1733 lignes de code Lancement : ./start-desktop.sh API utilisées : /admin/stats, /admin/users, /admin/groups, /admin/config, /admin/devices/list TODO : WebSocket VU-mètres, icônes, tray menu, graphiques monitoring
334 lines
7.9 KiB
JavaScript
334 lines
7.9 KiB
JavaScript
/**
|
|
* PTT Live Desktop - Main Process
|
|
* Intègre le serveur Node.js existant dans une application Electron
|
|
*/
|
|
|
|
const { app, BrowserWindow, ipcMain, Menu, Tray } = require('electron');
|
|
const path = require('path');
|
|
const { spawn } = require('child_process');
|
|
const http = require('http');
|
|
|
|
// État de l'application
|
|
let mainWindow = null;
|
|
let tray = null;
|
|
let serverProcess = null;
|
|
let serverStarted = false;
|
|
|
|
const SERVER_PORT = process.env.PORT || 3000;
|
|
const SERVER_URL = `http://localhost:${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'));
|
|
|
|
// DevTools en mode dev
|
|
if (isDev) {
|
|
mainWindow.webContents.openDevTools();
|
|
}
|
|
|
|
// Cleanup à la fermeture
|
|
mainWindow.on('closed', () => {
|
|
mainWindow = null;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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',
|
|
NODE_ENV: isDev ? 'development' : 'production'
|
|
}
|
|
});
|
|
|
|
serverProcess.stdout.on('data', (data) => {
|
|
const output = data.toString();
|
|
console.log('[Serveur]', output);
|
|
|
|
// Transmettre les logs au renderer
|
|
if (mainWindow) {
|
|
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) {
|
|
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();
|
|
console.error('[Serveur Error]', output);
|
|
|
|
if (mainWindow) {
|
|
mainWindow.webContents.send('server:log', {
|
|
level: 'error',
|
|
message: output.trim()
|
|
});
|
|
}
|
|
});
|
|
|
|
serverProcess.on('error', (error) => {
|
|
console.error('❌ Erreur démarrage serveur:', error);
|
|
serverStarted = false;
|
|
|
|
if (mainWindow) {
|
|
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) {
|
|
mainWindow.webContents.send('server:status', { running: false });
|
|
}
|
|
|
|
createTray(); // Mettre à jour tray
|
|
});
|
|
|
|
// Timeout de sécurité (10s)
|
|
setTimeout(() => {
|
|
if (!serverStarted && serverProcess) {
|
|
console.log('⏱️ Timeout démarrage serveur, on assume que c\'est OK');
|
|
serverStarted = true;
|
|
|
|
if (mainWindow) {
|
|
mainWindow.webContents.send('server:status', { running: true });
|
|
}
|
|
|
|
createTray();
|
|
resolve({ success: true, url: SERVER_URL });
|
|
}
|
|
}, 10000);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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) => {
|
|
http.get(`${SERVER_URL}/health`, (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 });
|
|
});
|
|
});
|
|
}
|
|
|
|
// ========== IPC Handlers ==========
|
|
|
|
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,
|
|
url: SERVER_URL
|
|
};
|
|
});
|
|
|
|
ipcMain.handle('server:ping', async () => {
|
|
return await pingServer();
|
|
});
|
|
|
|
// ========== App Lifecycle ==========
|
|
|
|
app.whenReady().then(async () => {
|
|
createWindow();
|
|
createTray();
|
|
|
|
// Démarrer le serveur automatiquement
|
|
console.log('🔄 Démarrage automatique du serveur...');
|
|
await startServer();
|
|
});
|
|
|
|
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);
|
|
});
|