feat: application desktop Electron avec interface graphique complète
- 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
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
Reference in New Issue
Block a user