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.
This commit is contained in:
2026-06-30 14:11:29 +02:00
parent 144caac183
commit 22bb66b680
6 changed files with 101 additions and 34 deletions
+19 -1
View File
@@ -8,6 +8,7 @@ const path = require('path');
const { spawn } = require('child_process'); const { spawn } = require('child_process');
const http = require('http'); const http = require('http');
const https = require('https'); const https = require('https');
const QRCode = require('qrcode');
const setupHelper = require('./setup-helper'); const setupHelper = require('./setup-helper');
// État de l'application // État de l'application
@@ -22,7 +23,10 @@ const SERVER_PORT = process.env.PORT || 3000;
// lancement) ; ENABLE_HTTPS=false permet de revenir explicitement en HTTP // lancement) ; ENABLE_HTTPS=false permet de revenir explicitement en HTTP
const ENABLE_HTTPS = process.env.ENABLE_HTTPS !== 'false'; const ENABLE_HTTPS = process.env.ENABLE_HTTPS !== 'false';
const SERVER_PROTOCOL = ENABLE_HTTPS ? 'https' : 'http'; const SERVER_PROTOCOL = ENABLE_HTTPS ? 'https' : 'http';
const SERVER_URL = `${SERVER_PROTOCOL}://localhost:${SERVER_PORT}`; // 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'); const isDev = process.argv.includes('--dev');
/** /**
@@ -339,6 +343,7 @@ app.whenReady().then(async () => {
return { return {
running: health.success, running: health.success,
health: health.data, health: health.data,
error: health.error,
url: SERVER_URL url: SERVER_URL
}; };
}); });
@@ -347,6 +352,19 @@ app.whenReady().then(async () => {
return await pingServer(); 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 // Créer fenêtre
createWindow(); createWindow();
createTray(); createTray();
+8 -1
View File
@@ -6,9 +6,10 @@
const { contextBridge, ipcRenderer } = require('electron'); const { contextBridge, ipcRenderer } = require('electron');
// Même logique que dans main.js : doit rester synchronisé avec SERVER_URL // Même logique que dans main.js : doit rester synchronisé avec SERVER_URL
// (127.0.0.1 : le serveur n'écoute qu'en IPv4, voir le commentaire dans main.js)
const SERVER_PORT = process.env.PORT || 3000; const SERVER_PORT = process.env.PORT || 3000;
const ENABLE_HTTPS = process.env.ENABLE_HTTPS !== 'false'; const ENABLE_HTTPS = process.env.ENABLE_HTTPS !== 'false';
const SERVER_URL = `${ENABLE_HTTPS ? 'https' : 'http'}://localhost:${SERVER_PORT}`; const SERVER_URL = `${ENABLE_HTTPS ? 'https' : 'http'}://127.0.0.1:${SERVER_PORT}`;
// Exposer l'API au renderer de manière sécurisée // Exposer l'API au renderer de manière sécurisée
contextBridge.exposeInMainWorld('electronAPI', { contextBridge.exposeInMainWorld('electronAPI', {
@@ -30,6 +31,12 @@ contextBridge.exposeInMainWorld('electronAPI', {
} }
}, },
// QR Code (généré côté Main Process, pas de dépendance CDN)
generateQRCode: (text) => ipcRenderer.invoke('qrcode:generate', text),
// IP réseau locale (même détection que pour les certificats mkcert)
getNetworkIP: () => ipcRenderer.invoke('network:ip'),
// Helpers // Helpers
platform: process.platform, platform: process.platform,
version: process.env.npm_package_version || '0.3.0' version: process.env.npm_package_version || '0.3.0'
+24 -25
View File
@@ -194,6 +194,10 @@ function updateServerStatus(running) {
// Déconnecter WebSocket audio levels // Déconnecter WebSocket audio levels
disconnectAudioLevelsWS(); disconnectAudioLevelsWS();
// QR code obsolète tant que le serveur est arrêté : revenir au placeholder
document.getElementById('qr-code').removeAttribute('src');
document.getElementById('client-url').textContent = '--';
} }
} }
@@ -362,31 +366,29 @@ async function generateQRCode() {
// Détecter l'IP réseau (depuis hostname ou config) // Détecter l'IP réseau (depuis hostname ou config)
const networkIP = await getNetworkIP(); const networkIP = await getNetworkIP();
const clientUrl = `https://${networkIP}:5173`; // Mode dev Vite // En prod (Electron), le client buildé est servi par le serveur Express
// lui-même (même port que l'API), pas par Vite (port 5173, dev only)
// API_BASE pointe sur 127.0.0.1 (loopback, pour le ping interne) :
// on ne réutilise que protocole + port, l'IP doit être celle du réseau local
const serverOrigin = new URL(API_BASE);
const clientUrl = `${serverOrigin.protocol}//${networkIP}:${serverOrigin.port}`;
document.getElementById('client-url').textContent = clientUrl; document.getElementById('client-url').textContent = clientUrl;
// Générer QR Code // Générer QR Code (rendu côté Main Process, pas de dépendance réseau/CDN)
const canvas = document.getElementById('qr-code'); const img = document.getElementById('qr-code');
if (canvas && window.QRCode) { if (img) {
QRCode.toCanvas(canvas, clientUrl, { const result = await window.electronAPI.generateQRCode(clientUrl);
width: 256, if (result.success) {
margin: 2, img.src = result.dataUrl;
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é'); console.log('✅ QR Code généré');
} else {
console.error('Erreur génération QR Code:', result.error);
} }
});
} }
} catch (error) { } catch (error) {
console.error('Erreur récupération URL:', error); console.error('Erreur récupération URL:', error);
document.getElementById('client-url').textContent = 'https://localhost:5173'; document.getElementById('client-url').textContent = API_BASE;
} }
// Bouton copier URL (setup une seule fois) // Bouton copier URL (setup une seule fois)
@@ -402,15 +404,12 @@ async function generateQRCode() {
} }
async function getNetworkIP() { async function getNetworkIP() {
// Méthode 1 : depuis l'API serveur (qui détecte déjà l'IP) // Détection via le Main Process (même logique que pour les certs mkcert) :
// /admin/config renvoie la valeur YAML brute ("AUTO"), jamais l'IP résolue,
// donc inutilisable ici.
try { try {
const config = await apiCall('/admin/config'); const ip = await window.electronAPI.getNetworkIP();
if (config && config.server && config.server.livekit && config.server.livekit.url) { if (ip) return ip;
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) { } catch (error) {
console.error('Erreur détection IP:', error); console.error('Erreur détection IP:', error);
} }
+8 -3
View File
@@ -78,7 +78,13 @@
<div class="section"> <div class="section">
<h3>📱 Connexion rapide clients</h3> <h3>📱 Connexion rapide clients</h3>
<div class="qr-container"> <div class="qr-container">
<canvas id="qr-code" width="256" height="256"></canvas> <div class="qr-wrapper">
<img id="qr-code" width="256" height="256" alt="QR Code connexion" />
<div class="qr-placeholder" id="qr-placeholder">
<span class="qr-placeholder-icon">📷</span>
<span>En attente du démarrage du serveur</span>
</div>
</div>
<div class="qr-info"> <div class="qr-info">
<p><strong>URL clients :</strong></p> <p><strong>URL clients :</strong></p>
<p class="url-text" id="client-url">--</p> <p class="url-text" id="client-url">--</p>
@@ -180,8 +186,7 @@
</main> </main>
</div> </div>
<!-- QR Code Library --> <!-- Le QR Code est généré côté Main Process (lib qrcode Node), pas de dépendance CDN -->
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js"></script>
<script src="app.js"></script> <script src="app.js"></script>
</body> </body>
</html> </html>
-2
View File
@@ -1,2 +0,0 @@
// Placeholder - QR Code sera généré via CDN
// En production, utiliser une lib locale ou CDN
+40
View File
@@ -230,11 +230,51 @@ body {
align-items: center; align-items: center;
} }
.qr-wrapper {
position: relative;
width: 256px;
height: 256px;
flex-shrink: 0;
}
#qr-code { #qr-code {
display: none;
width: 256px;
height: 256px;
border: 4px solid white; border: 4px solid white;
border-radius: 8px; border-radius: 8px;
} }
/* L'image n'a un attribut src qu'une fois le QR code généré */
#qr-code[src] {
display: block;
}
#qr-code[src] ~ .qr-placeholder {
display: none;
}
.qr-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.5rem;
width: 100%;
height: 100%;
border: 2px dashed var(--border-color);
border-radius: 8px;
color: var(--text-secondary);
text-align: center;
padding: 1rem;
box-sizing: border-box;
}
.qr-placeholder-icon {
font-size: 2.5rem;
opacity: 0.5;
}
.qr-info { .qr-info {
flex: 1; flex: 1;
} }