/** * PTT Live Desktop - Renderer Process Logic */ const API_BASE = 'http://localhost:3000'; // État global let serverRunning = false; let statsInterval = null; let logsBuffer = []; // ========== Initialisation ========== document.addEventListener('DOMContentLoaded', async () => { console.log('🚀 Interface Electron chargĂ©e'); // Setup navigation setupNavigation(); // Setup contrĂŽles serveur setupServerControls(); // Setup logs listener setupLogsListener(); // VĂ©rifier le statut initial du serveur await checkServerStatus(); // Charger les donnĂ©es initiales loadInitialData(); }); // ========== Navigation ========== function setupNavigation() { const navItems = document.querySelectorAll('.nav-item'); const views = document.querySelectorAll('.view'); navItems.forEach(item => { item.addEventListener('click', () => { const targetView = item.dataset.view; // Mettre Ă  jour l'Ă©tat actif navItems.forEach(nav => nav.classList.remove('active')); item.classList.add('active'); views.forEach(view => view.classList.remove('active')); document.getElementById(`view-${targetView}`).classList.add('active'); // Charger les donnĂ©es de la vue loadViewData(targetView); }); }); } // ========== ContrĂŽles Serveur ========== function setupServerControls() { const btnStart = document.getElementById('btn-start'); const btnStop = document.getElementById('btn-stop'); btnStart.addEventListener('click', async () => { btnStart.disabled = true; btnStart.textContent = 'DĂ©marrage...'; try { const result = await window.electronAPI.server.start(); console.log('RĂ©sultat dĂ©marrage:', result); if (result.success) { showNotification('Serveur dĂ©marrĂ© avec succĂšs', 'success'); } else { showNotification('Erreur dĂ©marrage: ' + result.message, 'error'); } } catch (error) { console.error('Erreur dĂ©marrage serveur:', error); showNotification('Erreur dĂ©marrage serveur', 'error'); } btnStart.disabled = false; btnStart.textContent = 'DĂ©marrer'; await checkServerStatus(); }); btnStop.addEventListener('click', async () => { btnStop.disabled = true; btnStop.textContent = 'ArrĂȘt...'; try { const result = await window.electronAPI.server.stop(); console.log('RĂ©sultat arrĂȘt:', result); if (result.success) { showNotification('Serveur arrĂȘtĂ©', 'info'); } } catch (error) { console.error('Erreur arrĂȘt serveur:', error); showNotification('Erreur arrĂȘt serveur', 'error'); } btnStop.disabled = false; btnStop.textContent = 'ArrĂȘter'; await checkServerStatus(); }); // Listener status depuis Main Process window.electronAPI.server.onStatus((data) => { console.log('Status update:', data); updateServerStatus(data.running); }); } function setupLogsListener() { window.electronAPI.server.onLog((logData) => { addLogEntry(logData); }); // Bouton clear logs document.getElementById('btn-clear-logs').addEventListener('click', () => { logsBuffer = []; renderLogs(); }); // Filtre niveau de log document.getElementById('log-level-filter').addEventListener('change', (e) => { renderLogs(e.target.value); }); } async function checkServerStatus() { try { const status = await window.electronAPI.server.status(); console.log('Status:', status); updateServerStatus(status.running); if (status.running) { startStatsPolling(); } else { stopStatsPolling(); } } catch (error) { console.error('Erreur check status:', error); updateServerStatus(false); } } function updateServerStatus(running) { serverRunning = running; const indicator = document.getElementById('status-indicator'); const statusText = document.getElementById('status-text'); const btnStart = document.getElementById('btn-start'); const btnStop = document.getElementById('btn-stop'); if (running) { indicator.textContent = '🟱'; statusText.textContent = 'Actif'; btnStart.disabled = true; btnStop.disabled = false; } else { indicator.textContent = 'âšȘ'; statusText.textContent = 'ArrĂȘtĂ©'; btnStart.disabled = false; btnStop.disabled = true; } } // ========== Polling Stats ========== function startStatsPolling() { if (statsInterval) return; // Poll toutes les 2 secondes statsInterval = setInterval(async () => { if (serverRunning) { await fetchStats(); await fetchUsers(); } }, 2000); // Premier fetch immĂ©diat fetchStats(); fetchUsers(); } function stopStatsPolling() { if (statsInterval) { clearInterval(statsInterval); statsInterval = null; } } // ========== API Calls ========== async function apiCall(endpoint) { try { const response = await fetch(`${API_BASE}${endpoint}`); if (!response.ok) throw new Error(`HTTP ${response.status}`); return await response.json(); } catch (error) { console.error(`API Error (${endpoint}):`, error); return null; } } async function fetchStats() { const data = await apiCall('/admin/stats'); if (!data) return; // Mettre Ă  jour les stats cards document.getElementById('stat-uptime').textContent = formatUptime(data.uptime); document.getElementById('stat-users').textContent = data.activeConnections || 0; document.getElementById('stat-total-connections').textContent = data.totalConnections || 0; // Groupes actifs (nĂ©cessite /admin/groups) const groups = await apiCall('/admin/groups'); if (groups) { document.getElementById('stat-groups').textContent = groups.groups?.length || 0; } } async function fetchUsers() { const data = await apiCall('/admin/users'); if (!data) return; const container = document.getElementById('users-list'); if (!data.users || data.users.length === 0) { container.innerHTML = '

Aucun utilisateur connecté

'; return; } container.innerHTML = data.users.map(user => `
đŸ‘€

${user.username}

Groupe: ${user.groupId} ‱ ConnectĂ©: ${formatTime(user.connectedAt)}

${user.groupId}
`).join(''); } async function fetchDevices() { const data = await apiCall('/admin/devices/list'); if (!data) return; const inputSelect = document.getElementById('input-device'); const outputSelect = document.getElementById('output-device'); // Remplir les selects inputSelect.innerHTML = data.inputs.map(device => `` ).join(''); outputSelect.innerHTML = data.outputs.map(device => `` ).join(''); } async function fetchGroups() { const data = await apiCall('/admin/groups'); if (!data) return; const container = document.getElementById('groups-list'); if (!data.groups || data.groups.length === 0) { container.innerHTML = '

Aucun groupe configuré

'; return; } container.innerHTML = data.groups.map(group => `

${group.name}

Bitrate: ${group.audioBitrate || 96} kbps ‱ ID: ${group.id}

`).join(''); } async function fetchConfig() { const data = await apiCall('/admin/config'); if (!data) return; // Remplir les champs de config audio if (data.audio) { const sampleRateSelect = document.getElementById('sample-rate'); if (sampleRateSelect) { sampleRateSelect.value = data.audio.sampleRate || 48000; } const bitrateInput = document.getElementById('default-bitrate'); if (bitrateInput) { bitrateInput.value = data.audio.defaultBitrate || 96; } const jitterInput = document.getElementById('jitter-buffer'); if (jitterInput) { jitterInput.value = data.audio.jitterBufferMs || 40; } } } // ========== QR Code ========== async function generateQRCode() { // RĂ©cupĂ©rer l'IP rĂ©seau depuis le serveur const status = await window.electronAPI.server.status(); if (!status || !status.running) { document.getElementById('client-url').textContent = 'Serveur non dĂ©marrĂ©'; return; } // RĂ©cupĂ©rer l'URL depuis l'API try { const response = await fetch(`${API_BASE}/health`); const data = await response.json(); // DĂ©tecter l'IP rĂ©seau (depuis hostname ou config) const networkIP = await getNetworkIP(); const clientUrl = `https://${networkIP}:5173`; // Mode dev Vite document.getElementById('client-url').textContent = clientUrl; // GĂ©nĂ©rer QR Code const canvas = document.getElementById('qr-code'); if (canvas && window.QRCode) { QRCode.toCanvas(canvas, clientUrl, { width: 256, margin: 2, color: { dark: '#000000', light: '#ffffff' } }, (error) => { if (error) { console.error('Erreur gĂ©nĂ©ration QR Code:', error); } else { console.log('✅ QR Code gĂ©nĂ©rĂ©'); } }); } } catch (error) { console.error('Erreur rĂ©cupĂ©ration URL:', error); document.getElementById('client-url').textContent = 'https://localhost:5173'; } // Bouton copier URL (setup une seule fois) const btnCopy = document.getElementById('btn-copy-url'); if (btnCopy && !btnCopy.dataset.initialized) { btnCopy.dataset.initialized = 'true'; btnCopy.addEventListener('click', () => { const url = document.getElementById('client-url').textContent; navigator.clipboard.writeText(url); showNotification('URL copiĂ©e !', 'success'); }); } } async function getNetworkIP() { // MĂ©thode 1 : depuis l'API serveur (qui dĂ©tecte dĂ©jĂ  l'IP) try { const config = await apiCall('/admin/config'); if (config && config.server && config.server.livekit && config.server.livekit.url) { const url = config.server.livekit.url; // Extraire l'IP depuis ws://IP:7880 const match = url.match(/ws:\/\/([^:]+):/); if (match) return match[1]; } } catch (error) { console.error('Erreur dĂ©tection IP:', error); } // Fallback : localhost return 'localhost'; } // ========== Logs ========== function addLogEntry(logData) { const entry = { timestamp: new Date().toISOString(), level: logData.level || 'info', message: logData.message }; logsBuffer.unshift(entry); // Garder max 500 logs if (logsBuffer.length > 500) { logsBuffer = logsBuffer.slice(0, 500); } renderLogs(); } function renderLogs(levelFilter = '') { const container = document.getElementById('logs-container'); let logs = logsBuffer; // Filtrer par niveau si nĂ©cessaire if (levelFilter) { logs = logs.filter(log => log.level === levelFilter); } if (logs.length === 0) { container.innerHTML = '

Aucun log

'; return; } container.innerHTML = logs.map(log => `
${formatLogTime(log.timestamp)} ${log.level} ${escapeHtml(log.message)}
`).join(''); // Scroll vers le bas (dernier log) container.scrollTop = 0; } // ========== Chargement donnĂ©es ========== async function loadInitialData() { if (!serverRunning) return; await fetchStats(); await fetchUsers(); await generateQRCode(); } async function loadViewData(view) { if (!serverRunning) return; switch (view) { case 'dashboard': await fetchStats(); await fetchUsers(); await generateQRCode(); break; case 'config': await fetchDevices(); await fetchConfig(); break; case 'groups': await fetchGroups(); break; case 'monitoring': // TODO: charger VU-mĂštres WebSocket break; case 'logs': renderLogs(); break; } } // ========== Boutons de sauvegarde ========== document.addEventListener('DOMContentLoaded', () => { // Sauvegarder device audio const btnSaveDevice = document.getElementById('btn-save-device'); if (btnSaveDevice) { btnSaveDevice.addEventListener('click', async () => { const inputDeviceId = document.getElementById('input-device').value; const outputDeviceId = document.getElementById('output-device').value; try { const response = await fetch(`${API_BASE}/admin/audio/device`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ inputDeviceId, outputDeviceId }) }); if (response.ok) { showNotification('PĂ©riphĂ©rique audio configurĂ©', 'success'); } else { showNotification('Erreur configuration', 'error'); } } catch (error) { console.error('Erreur save device:', error); showNotification('Erreur rĂ©seau', 'error'); } }); } // Sauvegarder config audio const btnSaveAudio = document.getElementById('btn-save-audio'); if (btnSaveAudio) { btnSaveAudio.addEventListener('click', async () => { const sampleRate = parseInt(document.getElementById('sample-rate').value); const defaultBitrate = parseInt(document.getElementById('default-bitrate').value); const jitterBufferMs = parseInt(document.getElementById('jitter-buffer').value); try { const response = await fetch(`${API_BASE}/admin/config/audio`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sampleRate, defaultBitrate, jitterBufferMs }) }); if (response.ok) { showNotification('Configuration sauvegardĂ©e', 'success'); } else { showNotification('Erreur sauvegarde', 'error'); } } catch (error) { console.error('Erreur save config:', error); showNotification('Erreur rĂ©seau', 'error'); } }); } // Ajouter groupe const btnAddGroup = document.getElementById('btn-add-group'); if (btnAddGroup) { btnAddGroup.addEventListener('click', async () => { const name = prompt('Nom du groupe:'); if (!name) return; try { const response = await fetch(`${API_BASE}/admin/groups`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, audioBitrate: 96 }) }); if (response.ok) { showNotification('Groupe créé', 'success'); await fetchGroups(); } else { showNotification('Erreur crĂ©ation groupe', 'error'); } } catch (error) { console.error('Erreur add group:', error); showNotification('Erreur rĂ©seau', 'error'); } }); } }); // ========== Helpers ========== function formatUptime(seconds) { if (!seconds) return '--'; const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = Math.floor(seconds % 60); return `${h}h ${m}m ${s}s`; } function formatTime(isoString) { if (!isoString) return '--'; const date = new Date(isoString); return date.toLocaleTimeString('fr-FR'); } function formatLogTime(isoString) { if (!isoString) return '--'; const date = new Date(isoString); return date.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function showNotification(message, type = 'info') { console.log(`[${type.toUpperCase()}] ${message}`); const container = document.getElementById('toast-container'); if (!container) return; // IcĂŽnes par type const icons = { success: '✅', error: '❌', warning: '⚠', info: 'â„č' }; // CrĂ©er le toast const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.innerHTML = ` ${icons[type] || icons.info} ${escapeHtml(message)} `; // Ajouter au container container.appendChild(toast); // Bouton fermer const closeBtn = toast.querySelector('.toast-close'); closeBtn.addEventListener('click', () => { toast.remove(); }); // Auto-remove aprĂšs 5 secondes setTimeout(() => { if (toast.parentElement) { toast.style.animation = 'slideIn 0.3s ease-out reverse'; setTimeout(() => toast.remove(), 300); } }, 5000); }