23 Commits

Author SHA1 Message Date
benoit 51245db256 feat: gestion groupes sans serveur via IPC + lecture YAML directe
- Ajout du package yaml dans electron pour parser config.yaml
- Handlers IPC groups:list/create/update/delete lisent/écrivent config.yaml
  directement depuis le Main Process (sans serveur requis)
- fetchGroups() utilise toujours IPC (plus d'appel REST pour la lecture)
- editGroup/deleteGroup/addGroup : REST API si serveur actif, IPC sinon
- loadViewData : onglet Groupes chargé même si serveur arrêté
- Note affichée quand le serveur est arrêté : modifications appliquées
  au prochain démarrage
2026-07-01 13:36:12 +02:00
benoit b3fbe31a2d fix: remplacer prompt/confirm par modals HTML (non supportés dans Electron)
Electron bloque prompt() et confirm() dans le renderer process.
Ajout d'un showModal() générique (champs configurables, mode confirmation)
avec overlay, focus auto, Escape/Enter, et fermeture en cliquant l'overlay.
Tous les appels prompt/confirm dans editGroup, deleteGroup et addGroup
sont migrés vers showModal.
2026-07-01 13:28:08 +02:00
benoit 955bfdfe07 feat: activer boutons Groupes et ajouter export logs/config desktop
- Onglet Groupes : boutons Modifier et Supprimer fonctionnels via délégation
  d'événements et data-attributes ; slugify() côté client synchronisé avec
  le serveur ; classe CSS btn-danger pour Supprimer
- Export logs : bouton "Exporter JSON" dans l'onglet Logs (filtre niveau actif)
- Export/Import config.yaml : section dédiée dans Configuration avec dialog
  système (backup automatique .bak avant import) via IPC Electron
  (config:export / config:import dans main.js + preload.js)
2026-07-01 13:21:59 +02:00
benoit a7a488403f Update documentation 2026-06-30 14:18:08 +02:00
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
benoit 144caac183 fix: activer HTTPS par défaut dans l'app Electron et fiabiliser les appels au serveur local
ENABLE_HTTPS était lu depuis l'environnement sans jamais être positionné par le
flow Electron (start-desktop.sh → electron .), donc le serveur enfant tournait
toujours en HTTP malgré le setup mkcert automatique au premier lancement.
ENABLE_HTTPS est désormais activé par défaut (ENABLE_HTTPS=false pour revenir
en HTTP explicitement).

Corrections induites par ce changement de protocole par défaut :
- pingServer() utilisait le module http en dur même en HTTPS ; bascule sur
  https avec rejectUnauthorized: false (ping local vers notre propre process
  enfant, le module https de Node ne lisant pas le trousseau macOS où mkcert
  installe sa CA, contrairement à Safari/Chrome/Electron renderer).
- Le dashboard (electron/ui/app.js) avait l'URL de l'API et celle du
  WebSocket VU-mètres codées en dur en http/ws ; elles utilisent maintenant
  l'URL réelle exposée par preload.js (serverUrl), cohérente avec le
  protocole effectif du serveur.
2026-06-30 13:45:33 +02:00
benoit b7911badb2 fix: corriger conflit upgrade WebSocket entre proxy LiveKit et audio-levels
AudioLevelsServer s'auto-attachait à l'événement 'upgrade' du serveur HTTP
via la lib ws (server + path), en plus du listener manuel du proxy LiveKit.
Pour toute connexion /livekit, les deux listeners s'exécutaient : le proxy
LiveKit aboutissait bien côté upstream, mais le listener ws (path
/audio-levels ne matchant pas) appelait abortHandshake(socket, 400) sur le
même socket juste après, cassant la connexion côté client en HTTPS prod.

AudioLevelsServer passe maintenant en noServer: true et expose
handleUpgrade(), appelée par un dispatcher 'upgrade' unique dans
server/index.js qui route explicitement par chemin (/livekit vs
/audio-levels).

Ajout de certs/ au .gitignore (clés privées SSL locales mkcert).
2026-06-30 13:29:09 +02:00
benoit dfe5db979a feat: ajout proxy HTTP pour routes REST LiveKit
Ajout proxy pour requetes HTTP LiveKit (ex: /rtc/validate).
Le client LiveKit fait d'abord une requete HTTP avant WebSocket.
2026-06-19 14:14:22 +02:00
benoit d3558388ad fix: remplacement http-proxy-middleware par http-proxy natif
http-proxy-middleware ne gere pas correctement WebSocket upgrade avec HTTPS.
Utilisation de http-proxy natif pour proxy direct WSS vers WS.

Modifications:
- Remplacement createProxyMiddleware par httpProxy.createProxyServer
- Gestion native de l'upgrade WebSocket
- Reecriture URL /livekit -> / pour LiveKit
- Logs info niveau pour debug facilite
2026-06-19 14:02:04 +02:00
benoit 8d2b83be0a fix: amelioration gestion upgrade WebSocket pour proxy LiveKit
Ajout logs debug pour tracer les requetes WebSocket upgrade.
Gestion explicite des chemins /livekit et /audio-levels.
2026-06-19 13:58:27 +02:00
benoit 861448f565 fix: utiliser proxy WSS pour LiveKit en mode HTTPS
Correction detection du protocole pour utiliser le bon endpoint LiveKit.

Modifications:
- client/src/App.jsx: detection HTTPS au lieu de import.meta.env.DEV
- Si HTTPS: utiliser wss://host/livekit (proxy)
- Si HTTP: utiliser URL LiveKit directe

Probleme resolu:
- Mixed content error (HTTPS ne peut pas WS)
- Fonctionne maintenant en prod HTTPS build
2026-06-19 13:52:47 +02:00
benoit 32158079c6 feat: ajout proxy WebSocket pour LiveKit en mode HTTPS
Permet au client PWA charge en HTTPS de se connecter a LiveKit via proxy WSS.

Modifications:
- server/index.js: ajout proxy http-proxy-middleware
- Route /livekit proxie vers http://localhost:7880
- Upgrade WebSocket active pour le proxy
- server/package.json: ajout dependance http-proxy-middleware

Fonctionnement:
- Client HTTPS se connecte a wss://localhost:3000/livekit
- Serveur Express proxie vers ws://localhost:7880
- Resout probleme mixed content (HTTPS ne peut pas WS)
2026-06-19 13:49:28 +02:00
benoit c21433b9eb feat: implementation WebSocket VU-metres dans interface desktop
Ajout connexion WebSocket temps reel pour monitoring audio dans l'app Electron.

Modifications:
- electron/ui/app.js: connexion WebSocket /audio-levels
- Rendu VU-metres pour inputs/groups/outputs
- Reconnexion automatique en cas de deconnexion
- Gestion cycle de vie (demarrage/arret serveur)
- electron/ui/styles.css: styles pour VU-metres
- Barres horizontales avec couleurs (vert/jaune/rouge)
- Indicateur peak temps reel
- Animation clipping si saturation
- DESKTOP-APP.md: marque TODO comme complete

Fonctionnalites:
- Affichage niveaux RMS et peak en dBFS
- Detection clipping avec animation
- Status connexion WebSocket visible
- Mise a jour 20 fois/seconde (50ms)
- Sections separees: entrees, groupes, sorties
2026-06-19 13:40:03 +02:00
benoit 865d40b7db fix: appliquer rendererReady check aux events stdout aussi
Complète le commit précédent : tous les webContents.send() vérifient rendererReady
2026-06-19 13:31:55 +02:00
benoit bc2d5a0940 fix: attendre que le renderer soit prêt avant d'envoyer events
Problème : mainWindow.webContents.send() appelé avant que la page soit chargée
→ Events perdus, interface ne se met pas à jour

Solution :
- Nouveau flag rendererReady
- Event 'did-finish-load' pour détecter quand le renderer est prêt
- Tous les webContents.send() vérifient maintenant rendererReady
- Envoi de l'état initial du serveur quand interface charge

Résultat : Interface reçoit toujours les updates d'état serveur
2026-06-19 13:31:38 +02:00
benoit ad214f644b fix: démarrage manuel et gestion état serveur
Corrections :
- Serveur NE démarre PLUS automatiquement au lancement
- Utilisateur doit cliquer "Démarrer" (contrôle explicite)
- Logs stderr LiveKit traités comme INFO (pas ERROR)
- Détection démarrage dans stderr aussi ("starting LiveKit server")
- Frontend : charge données seulement si serveur actif
- Polling démarre/arrête selon état serveur
- État initial : bouton Démarrer enabled, Arrêter disabled

Expérience :
1. Lancer app → Interface visible, serveur arrêté
2. Cliquer "Démarrer" → Serveur démarre
3. Dashboard se met à jour automatiquement
4. Cliquer "Arrêter" → Serveur s'arrête, polling stop

Plus de démarrage automatique surprise !
2026-06-19 13:29:11 +02:00
benoit f0cf363408 feat: setup automatique certificats SSL au premier lancement
Nouveau : setup-helper.js
- Détecte si mkcert est installé
- Installe mkcert automatiquement (Homebrew macOS, curl Linux)
- Installe CA locale (mkcert -install)
- Génère certificats pour localhost + IP réseau
- Détection automatique IP WiFi

Intégration dans main.js :
- Vérifie certificats au démarrage
- Si absents : dialog onboarding + setup auto
- Dialog progress "Configuration en cours..."
- Dialog success avec IP réseau
- Dialog error si échec (fallback manuel)
- Ne démarre serveur que si certificats OK

Expérience utilisateur :
1. Lancer l'app (première fois)
2. Dialog : "Première utilisation, configuration..."
3. Cliquer "Continuer"
4. Attendre 1-2 min (installation mkcert + CA + certificats)
5. Dialog : "Configuration terminée, IP: 192.168.x.x"
6. Serveur démarre automatiquement

Prochaines fois : détection certificats OK → démarre direct

L'utilisateur n'a RIEN à faire manuellement !
2026-06-19 13:24:51 +02:00
benoit 8a7e98ae47 fix: déplacer handlers IPC dans app.whenReady()
Les handlers ipcMain.handle() doivent être définis après app.whenReady()
sinon ipcMain est undefined

Résout: TypeError: Cannot read properties of undefined (reading 'handle')
2026-06-19 13:14:54 +02:00
benoit 17afd6e5f1 fix: correction démarrage serveur dans Electron
- Logs LiveKit vont dans stderr (normal pour Go), ne pas traiter comme erreurs
- Transmettre tous les logs au renderer (stdout + stderr)
- Détecter "Serveur prêt" dans stdout pour confirmer démarrage
- Timeout augmenté à 15s avec health check avant de résoudre
- Suppression filtres logs verbeux qui cachaient les messages importants

Résout : serveur tué immédiatement après démarrage
2026-06-19 13:13:41 +02:00
benoit b65e6cc791 chore: suppression fichiers récap interdits par CLAUDE.md
Supprimés :
- ELECTRON_SUMMARY.md
- SSL-SOLUTION.md
- QUICKSTART-SSL.md (non commité)

Respect de la règle : pas de fichiers récapitulatifs markdown
2026-06-19 13:12:06 +02:00
benoit 1c5bdeddb5 feat: solution SSL 100% locale avec mkcert pour HTTPS de confiance
Problème résolu : certificats self-signed bloqués par navigateurs

Solution : mkcert génère certificats automatiquement approuvés
- CA locale installée sur système
- Certificats signés par cette CA
- Navigateurs font confiance automatiquement
- Pas de warnings SSL
- 100% local, pas de cloud/domaine

Nouveau script : setup-certificates.sh
- Installe mkcert (Homebrew/apt)
- Installe CA locale (mkcert -install)
- Détecte IP réseau automatiquement
- Génère certificats localhost + IP + *.local
- Configure server/.env (SSL_CERT, SSL_KEY)
- Configure client/.env (VITE_SERVER_URL)
- Met à jour vite.config.js avec HTTPS

Serveur modifié : server/index.js
- Lit certificats depuis process.env.SSL_CERT/SSL_KEY
- Fallback : ../certs/localhost.pem
- Message erreur si certificats introuvables

Documentation :
- SSL-SETUP.md : guide complet installation manuelle/auto
- SSL-SOLUTION.md : résumé technique
- README.md : ajout étape setup-certificates.sh

Résultat :
- Cadenas vert sur desktop (Chrome/Safari/Firefox)
- WebRTC fonctionne en HTTPS
- Smartphones : accepter certificat une fois (normal)
- Valable 10 ans, pas de renouvellement

Usage : ./setup-certificates.sh (2 minutes)
Ensuite : ./start.sh --dev ou ./start-desktop.sh
2026-06-19 13:10:19 +02:00
benoit 530c3a10b2 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
2026-06-19 11:04:29 +02:00
benoit 312d47d677 Merge pull request 'macos' (#1) from macos into main
Reviewed-on: #1
2026-06-18 16:20:05 +02:00
23 changed files with 4851 additions and 15 deletions
+3
View File
@@ -55,3 +55,6 @@ server.log
# Runtime files # Runtime files
/tmp/ptt-live.pid /tmp/ptt-live.pid
# Certificats SSL locaux (mkcert) - contiennent des clés privées
certs/
+43
View File
@@ -208,6 +208,11 @@ PTT Live/
./install.sh # Détecte OS, configure tout automatiquement ./install.sh # Détecte OS, configure tout automatiquement
# Démarrage rapide # Démarrage rapide
# Option 1 : Application Desktop (Interface graphique)
./start-desktop.sh # Lance l'app Electron avec dashboard
# Option 2 : Mode CLI (deux terminaux)
./start.sh --dev # Mode développement ./start.sh --dev # Mode développement
./start.sh # Mode production ./start.sh # Mode production
@@ -223,6 +228,44 @@ npm install
npm run dev npm run dev
``` ```
## Application Desktop (v0.3.0)
### Interface Electron
- **Main Process** : spawn serveur Node.js, IPC handlers
- **Renderer Process** : dashboard HTML/CSS/JS
- **Communication** : IPC sécurisé (contextBridge) + HTTP vers API admin
### Fonctionnalités
-**Dashboard** : stats temps réel, utilisateurs, QR Code
-**Configuration** : devices audio, sample rate, bitrate, jitter
-**Groupes** : CRUD complet via API admin
-**Monitoring** : logs filtrables (error/warn/info/debug)
-**Notifications** : toast visuelles avec auto-dismiss
- 🚧 **VU-mètres** : WebSocket audio levels (prévu)
### Structure
```
electron/
├── main.js # Main Process (spawn serveur)
├── preload.js # IPC bridge sécurisé
├── package.json # Config Electron + electron-builder
└── ui/
├── index.html # Interface dashboard
├── styles.css # Dark theme
└── app.js # Logic + API calls
```
### Build & Distribution
```bash
cd electron
npm run build:mac # → dist/PTT Live Server.dmg
npm run build:linux # → dist/PTT Live Server.AppImage
```
Voir [DESKTOP-APP.md](DESKTOP-APP.md) pour la doc complète.
---
## Fonctionnalités de portabilité (v0.2.1) ## Fonctionnalités de portabilité (v0.2.1)
### Installation zéro-config ### Installation zéro-config
+438
View File
@@ -0,0 +1,438 @@
# PTT Live - Application Desktop Server
Application Electron pour gérer le serveur PTT Live avec interface graphique complète.
## 📸 Aperçu
L'application desktop intègre :
-**Dashboard temps réel** : stats, utilisateurs, QR Code (généré côté Main Process, sans dépendance CDN)
-**HTTPS automatique** : certificats locaux mkcert installés au premier lancement
-**Configuration audio** : sélection devices, sample rate, bitrate
-**Gestion groupes** : CRUD complet avec API
-**Monitoring** : VU-mètres temps réel via WebSocket, logs filtrables
-**Contrôle serveur** : démarrage manuel/arrêt avec feedback visuel
---
## 🚀 Démarrage Rapide
```bash
# Depuis la racine du projet
./start-desktop.sh
# OU manuellement
cd electron
npm start
```
Au premier lancement, l'app configure automatiquement les certificats HTTPS locaux (mkcert) — voir [HTTPS et certificats](#-https-et-certificats). Le serveur PTT Live **ne démarre pas automatiquement** : cliquez sur "Démarrer" dans le dashboard pour le lancer.
---
## 📦 Installation
Les dépendances sont déjà installées. Si nécessaire :
```bash
cd electron
npm install
```
---
## 🎯 Utilisation
### 1. Dashboard
**Stats temps réel** :
- Uptime serveur
- Nombre d'utilisateurs connectés
- Groupes actifs
- Total connexions
**QR Code** :
- Généré côté Main Process (lib `qrcode`, pas de CDN externe — fonctionne sans accès Internet sur le WiFi d'un événement)
- IP réseau détectée par le Main Process (même logique que pour les certificats mkcert)
- URL construite à partir du protocole/port réels du serveur (HTTPS par défaut)
- Scanner depuis smartphone pour connexion rapide
- Bouton copier URL
- Placeholder visuel tant que le serveur est arrêté ou qu'aucun QR code n'a été généré
**Utilisateurs** :
- Liste en temps réel
- Groupe de chaque utilisateur
- Heure de connexion
### 2. Configuration Audio
**Périphériques** :
- Sélection input/output depuis dropdown auto-détecté
- Support macOS (CoreAudio), Linux (JACK/PipeWire)
- Appliquer instantanément (bridge audio rechargé)
**Paramètres** :
- Sample Rate : 44.1 / 48 / 96 kHz
- Bitrate par défaut : 32-320 kbps
- Jitter Buffer : 20-100 ms
### 3. Gestion Groupes
- **Créer** : bouton " Nouveau groupe"
- **Modifier** : depuis la liste (nom, bitrate)
- **Supprimer** : confirmation requise
- Sauvegardé automatiquement dans `config.yaml`
### 4. Monitoring
**VU-Mètres** :
- Niveaux audio par canal (input/output) et par groupe
- Temps réel via WebSocket (`/audio-levels`, même port que l'API)
- Reconnexion automatique si la connexion WebSocket tombe
### 5. Logs
- Logs serveur en temps réel
- Filtrage par niveau (error/warn/info/debug)
- Bouton "Effacer"
- Format timestamp + niveau + message
---
## 🏗️ Architecture Technique
```
┌─────────────────────────────────────────────────┐
│ ELECTRON APP (Desktop) │
│ │
│ ┌───────────────────────────────────────────┐ │
│ │ MAIN PROCESS (Node.js) │ │
│ │ │ │
│ │ • spawn server/index.js │ │
│ │ • IPC handlers (start/stop/status) │ │
│ │ • Tray icon (macOS/Linux) │ │
│ │ • Logs forwarding → Renderer │ │
│ └───────────────────────────────────────────┘ │
│ ↕ IPC │
│ ┌───────────────────────────────────────────┐ │
│ │ RENDERER PROCESS (Frontend) │ │
│ │ │ │
│ │ • HTML/CSS/JS (pas de framework) │ │
│ │ • Fetch API REST :3000/admin/* │ │
│ │ • WebSocket audio levels (live) │ │
│ │ • QR Code (data URL via IPC) │ │
│ └───────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
↕ HTTPS (127.0.0.1, certs mkcert)
┌─────────────────────────────────────────────────┐
│ SERVEUR PTT LIVE (spawned) │
│ │
│ • LiveKit Server (binaire Go) :7880 │
│ • Audio Bridge Manager │
│ • API REST Express :3000 (HTTPS) │
│ • Proxy HTTP + WS → LiveKit (/livekit/*) │
│ • WebSocket Audio Levels (/audio-levels) │
└─────────────────────────────────────────────────┘
```
Le proxy `/livekit/*` (http-proxy natif) permet aux clients de joindre LiveKit via le même port/certificat HTTPS que l'API, sans exposer le port 7880 séparément. Le serveur Express dispatch lui-même les événements `upgrade` (un seul listener) entre le proxy LiveKit et le WebSocket audio-levels, qui partagent le même port.
---
## 🔌 API Consommées
L'interface desktop utilise toutes les routes admin existantes :
| Endpoint | Méthode | Usage |
|----------|---------|-------|
| `/admin/stats` | GET | Dashboard metrics |
| `/admin/users` | GET | Liste utilisateurs |
| `/admin/groups` | GET | Liste groupes |
| `/admin/groups` | POST | Créer groupe |
| `/admin/groups/:id` | PUT | Modifier groupe |
| `/admin/groups/:id` | DELETE | Supprimer groupe |
| `/admin/config` | GET | Config complète |
| `/admin/config/audio` | PUT | Mettre à jour audio |
| `/admin/audio/devices` | GET | Énumérer devices |
| `/admin/audio/device` | POST | Sélectionner device |
| `/admin/devices/list` | GET | Auto-détection (macOS/Linux) |
| `/admin/logs` | GET | Logs serveur |
| `/health` | GET | Health check |
| `/livekit/*` | ALL | Proxy HTTP vers LiveKit Server (port 7880) |
WebSocket :
- `wss://127.0.0.1:3000/audio-levels` → VU-mètres temps réel
- `wss://127.0.0.1:3000/livekit/*` → Proxy WebSocket signaling LiveKit (clients PWA)
---
## 🔒 HTTPS et certificats
L'app est en HTTPS par défaut (`ENABLE_HTTPS=false` pour revenir en HTTP explicitement).
### Setup automatique (premier lancement)
Au premier démarrage, si `certs/localhost.pem` et `certs/localhost-key.pem` sont absents, `electron/setup-helper.js` :
1. Installe `mkcert` automatiquement (Homebrew sur macOS, téléchargement direct sur Linux)
2. Installe la CA locale (`mkcert -install`) dans le trousseau système
3. Détecte l'IP réseau et génère les certificats pour `localhost`, `127.0.0.1` et cette IP
4. Affiche des dialogs de progression/erreur (avec fallback manuel `./setup-certificates.sh`)
### Points d'attention
- **127.0.0.1, pas localhost** : le serveur écoute en IPv4 (`host: 0.0.0.0`), mais le Node embarqué par Electron peut résoudre `localhost` en IPv6 (`::1`) en priorité. `main.js` et `preload.js` utilisent donc `127.0.0.1` pour tous les appels internes (ping, health check) afin d'éviter des échecs silencieux.
- **Ping interne et `rejectUnauthorized`** : le module `https` de Node ne lit pas le trousseau système où mkcert installe sa CA (contrairement à Safari/Chrome/Electron renderer) ; `pingServer()` passe donc `rejectUnauthorized: false` pour son propre ping local.
- **Proxy LiveKit en HTTPS** : LiveKit Server local tourne en HTTP brut (port 7880) ; le proxy Express (`http-proxy`) fait le pont HTTPS ↔ HTTP côté clients.
---
## 📦 Build pour Distribution
### macOS
```bash
cd electron
npm run build:mac
```
Génère :
- `dist/mac/PTT Live Server.app`
- `dist/PTT Live Server-0.3.0.dmg`
### Linux
```bash
cd electron
npm run build:linux
```
Génère :
- `dist/PTT Live Server-0.3.0.deb`
- `dist/PTT Live Server-0.3.0.AppImage`
### Tester le build
```bash
# macOS
open dist/mac/PTT\ Live\ Server.app
# Linux
./dist/PTT\ Live\ Server-0.3.0.AppImage
```
---
## 🎨 Personnalisation
### Icônes
Placer les icônes dans `electron/assets/` :
```
electron/assets/
├── icon.icns # macOS (512x512 minimum)
├── icon.png # Linux (512x512)
└── tray-icon.png # Tray 22x22 ou 44x44 (retina)
```
Générer icônes depuis PNG :
```bash
# macOS .icns
iconutil -c icns assets/icon.iconset
# Linux .png
convert icon.png -resize 512x512 assets/icon.png
```
### Thème
Modifier `electron/ui/styles.css` :
```css
:root {
--bg-primary: #1a1a1a;
--accent-primary: #4a9eff;
/* ... */
}
```
---
## 🐛 Debug
### DevTools
Ouvrir automatiquement en mode dev :
```bash
cd electron
npm run dev
```
Ou manuellement dans `main.js` :
```javascript
mainWindow.webContents.openDevTools();
```
### Logs Console
**Main Process** :
```javascript
console.log('[Main]', ...); // Terminal qui a lancé npm start
```
**Renderer Process** :
```javascript
console.log('[Renderer]', ...); // DevTools → Console
```
**Serveur PTT Live** :
```javascript
// Transmis au Renderer via IPC
window.electronAPI.server.onLog((log) => {
console.log('[Serveur]', log);
});
```
### Erreurs courantes
**Port 3000 déjà utilisé** :
```bash
# Tuer le process
lsof -i :3000
kill -9 <PID>
# OU changer de port
PORT=3001 npm start
```
**Serveur ne démarre pas** :
- Vérifier que `server/index.js` existe
- Vérifier permissions LiveKit binaire
- Voir logs dans DevTools console
**Certificats SSL manquants / setup mkcert échoue** :
- Exécuter manuellement : `./setup-certificates.sh`
- Ou installer mkcert : https://github.com/FiloSottile/mkcert puis `mkcert -install`
- Vérifier la présence de `certs/localhost.pem` et `certs/localhost-key.pem`
**Statut serveur affiché à tort comme "arrêté"** :
- Vérifier que le ping utilise bien `127.0.0.1` (pas `localhost`, qui peut résoudre en IPv6 alors que le serveur n'écoute qu'en IPv4)
- En HTTPS, le ping interne ignore volontairement les erreurs de certificat (`rejectUnauthorized: false`) puisque Node ne lit pas le trousseau système où mkcert installe sa CA
**QR Code ne s'affiche pas** :
- Vérifier que le serveur tourne (le QR code est réinitialisé tant qu'il est arrêté)
- Le QR code est généré côté Main Process (IPC `qrcode:generate`), pas de dépendance réseau/CDN
---
## 🚧 TODO / Améliorations
### Priorité haute
- [x] **WebSocket VU-mètres** : implémenter connexion `/audio-levels`
- [ ] **Vraies icônes** : icns/png pour macOS/Linux
- [ ] **Tray icon** : avec menu contextuel fonctionnel
### Priorité moyenne
- [ ] **Graphiques monitoring** : Chart.js pour latence/bande passante
- [x] **Export logs** : bouton télécharger JSON (filtre niveau appliqué)
- [ ] **Matrice routing** : interface graphique drag & drop
- [x] **Export & import config** : bouton télécharger YAML et charger config (backup auto .bak)
### Priorité basse
- [ ] **Thème toggle** : dark/light mode
- [ ] **Auto-update** : electron-updater pour mises à jour
### Technique
- [ ] **Tests** : Spectron ou Playwright pour Electron
- [ ] **CI/CD** : GitHub Actions pour builds automatiques
- [ ] **Signature code** : macOS notarization + Linux AppImage signature
---
## 📝 Notes de Développement
### Structure Fichiers
```
electron/
├── main.js # Main Process
│ # - Spawn serveur
│ # - IPC handlers
│ # - Window management
│ # - Setup SSL au premier lancement
├── preload.js # IPC Bridge sécurisé
│ # - contextBridge
│ # - Expose electronAPI
├── setup-helper.js # Installation auto mkcert + génération certificats
│ # - Détection IP réseau
├── package.json # Config Electron + electron-builder
└── ui/ # Renderer Process (Frontend)
├── index.html # Structure UI
├── styles.css # Styles (dark theme)
└── app.js # Logic frontend (QR code reçu via IPC en data URL)
```
### Communication IPC
**Renderer → Main** :
```javascript
// Depuis ui/app.js
const result = await window.electronAPI.server.start();
```
**Main → Renderer** :
```javascript
// Depuis main.js
mainWindow.webContents.send('server:status', { running: true });
// Écouté dans ui/app.js
window.electronAPI.server.onStatus((data) => {
console.log('Status:', data);
});
```
### Sécurité
-**contextIsolation: true** : isole Node.js du renderer
-**nodeIntegration: false** : pas d'accès Node direct
-**preload.js** : whitelist API exposées via contextBridge
- ⚠️ **CSP manquant** : ajouter Content-Security-Policy en prod
---
## 🤝 Contribution
L'app desktop est modulaire et extensible :
1. **Ajouter une vue** : créer `<div id="view-xxx">` dans `index.html`
2. **Ajouter un handler IPC** : `ipcMain.handle()` dans `main.js`
3. **Exposer au renderer** : `contextBridge.exposeInMainWorld()` dans `preload.js`
4. **Appeler l'API** : fetch dans `ui/app.js`
---
## 📚 Ressources
- **Electron Docs** : https://www.electronjs.org/docs
- **electron-builder** : https://www.electron.build
- **LiveKit Server API** : https://docs.livekit.io
- **QR Code.js** : https://github.com/soldair/node-qrcode
---
## 📄 Licence
Même licence que PTT Live (MIT)
---
**Version** : 0.3.0
**Dernière mise à jour** : 2026-06-30
+27 -2
View File
@@ -8,18 +8,43 @@ Communiquez via smartphone (PWA) en WiFi, le serveur fait le pont avec l'install
## 🚀 Démarrage rapide ## 🚀 Démarrage rapide
### 🖥️ Application Desktop (Nouveau !)
**Interface graphique complète pour gérer le serveur** :
```bash
# Lancer l'application desktop
./start-desktop.sh
```
**Fonctionnalités** :
- Dashboard temps réel (stats, utilisateurs)
- Configuration audio (devices, bitrate)
- Gestion groupes (CRUD)
- QR Code pour connexion clients
- Logs serveur filtrables
📖 **Documentation complète** : [DESKTOP-APP.md](DESKTOP-APP.md)
---
### Installation Automatique (Recommandé) ### Installation Automatique (Recommandé)
**Un seul script pour tout installer** (détection automatique macOS/Linux) : **Un seul script pour tout installer** (détection automatique macOS/Linux) :
```bash ```bash
# Lancer l'installation portable # 1. Installer dépendances + LiveKit
./install.sh ./install.sh
# Démarrer le système # 2. Configurer certificats SSL locaux (NOUVEAU - requis pour HTTPS)
./setup-certificates.sh
# 3. Démarrer le système (mode CLI)
./start.sh --dev ./start.sh --dev
``` ```
🔐 **Certificats SSL** : Le script `setup-certificates.sh` génère des certificats **automatiquement approuvés** (pas de warnings navigateur). Voir [SSL-SETUP.md](SSL-SETUP.md)
**L'installeur configure automatiquement** : **L'installeur configure automatiquement** :
- LiveKit Server local (pas besoin de compte cloud) - LiveKit Server local (pas besoin de compte cloud)
- Détection et configuration IP réseau - Détection et configuration IP réseau
+375
View File
@@ -0,0 +1,375 @@
# 🔐 Configuration SSL 100% Locale - PTT Live
## Problème Résolu
**Avant** : Certificats self-signed bloqués par navigateurs
**Après** : Certificats locaux **automatiquement approuvés**
---
## Solution : mkcert
**mkcert** génère des certificats SSL locaux **de confiance** :
- ✅ Approuvés automatiquement par Chrome/Safari/Edge/Firefox
- ✅ Approuvés par le système (macOS/Linux)
- ✅ Pas besoin de clics "Accepter le risque"
- ✅ 100% local, pas de cloud, pas de domaine
---
## 🚀 Installation Automatique (Recommandée)
### Un seul script fait tout
```bash
# Depuis la racine du projet
./setup-certificates.sh
```
Ce script :
1. ✅ Installe `mkcert` (si pas déjà installé)
2. ✅ Installe la CA locale (Certificate Authority)
3. ✅ Génère certificats pour localhost + IP réseau
4. ✅ Configure automatiquement serveur et client
5. ✅ Crée les `.env` avec chemins certificats
**Temps : ~2 minutes**
---
## 📋 Ce qui est Créé
### Structure
```
PTT Live/
├── certs/ # Nouveau dossier
│ ├── localhost.pem # Certificat public
│ └── localhost-key.pem # Clé privée
├── server/.env # Mis à jour automatiquement
│ ├── SSL_CERT=/path/to/localhost.pem
│ └── SSL_KEY=/path/to/localhost-key.pem
└── client/
├── .env # Créé automatiquement
└── vite.config.js # Mis à jour avec HTTPS
```
### Certificats Générés Pour
- `localhost`
- `127.0.0.1`
- Votre **IP réseau** (ex: `192.168.1.10`)
- `*.local` (wildcard)
- `$(hostname).local`
---
## 🌐 URLs d'Accès
Après installation, accès HTTPS sans warnings :
```
Serveur : https://192.168.1.10:3000
Client : https://192.168.1.10:5173
QR Code : généré automatiquement au démarrage
```
---
## 📱 Smartphones (iOS/Android)
### Première Connexion
1. **Scanner le QR Code** affiché au démarrage du serveur
2. Le navigateur ouvre l'URL HTTPS
3. **Accepter le certificat** (une seule fois par appareil)
- iOS : Cliquer "Continuer" → "Visiter ce site web"
- Android : Cliquer "Avancé" → "Continuer vers le site"
4. La PWA se charge normalement
5. **Installer sur l'écran d'accueil** (recommandé)
### Pourquoi Accepter Manuellement sur Mobile ?
La CA locale est installée sur l'**ordinateur serveur**, pas sur le smartphone.
**Options** :
**A) Accepter à chaque appareil** (simple, rapide)
- Une seule fois par smartphone
- 2 clics
**B) Installer la CA sur les mobiles** (optionnel, avancé)
- iOS : Réglages → Général → VPN & Gestion → Profils
- Android : Paramètres → Sécurité → Certificats
💡 **Recommandation** : Option A (accepter manuellement), plus simple.
---
## 🛠️ Fonctionnement Technique
### 1. mkcert
```bash
# Installer CA locale (une fois par machine)
mkcert -install
# Générer certificats
mkcert localhost 192.168.1.10 *.local
# → Crée localhost.pem + localhost-key.pem
```
### 2. Serveur Express (HTTPS)
```javascript
// server/index.js
const https = require('https');
const fs = require('fs');
const httpsOptions = {
key: fs.readFileSync(process.env.SSL_KEY),
cert: fs.readFileSync(process.env.SSL_CERT)
};
https.createServer(httpsOptions, app).listen(3000);
```
### 3. Vite Dev Server (HTTPS)
```javascript
// client/vite.config.js
export default defineConfig({
server: {
https: {
key: fs.readFileSync('../certs/localhost-key.pem'),
cert: fs.readFileSync('../certs/localhost.pem')
}
}
});
```
---
## 🔧 Installation Manuelle (Si Script Échoue)
### macOS
```bash
# 1. Installer mkcert
brew install mkcert
brew install nss # Pour Firefox
# 2. Installer CA locale
mkcert -install
# 3. Créer dossier certificats
mkdir certs
cd certs
# 4. Générer certificats (remplacer IP)
mkcert localhost 127.0.0.1 192.168.1.10 *.local
# 5. Renommer
mv localhost+*.pem localhost.pem
mv localhost+*-key.pem localhost-key.pem
# 6. Configurer .env (voir ci-dessous)
```
### Linux
```bash
# 1. Installer dépendances
sudo apt-get install libnss3-tools # Debian/Ubuntu
# OU
sudo yum install nss-tools # RedHat/CentOS
# 2. Télécharger mkcert
curl -JLO "https://dl.filippo.io/mkcert/latest?for=linux/amd64"
chmod +x mkcert-v*-linux-amd64
sudo mv mkcert-v*-linux-amd64 /usr/local/bin/mkcert
# 3-6. Mêmes étapes que macOS
```
### Configuration Manuelle .env
**server/.env** :
```bash
USE_LOCAL_LIVEKIT=true
LIVEKIT_API_KEY=devkey
LIVEKIT_API_SECRET=secret
LIVEKIT_URL=AUTO
PORT=3000
ENABLE_HTTPS=true
# Chemins ABSOLUS
SSL_CERT=/Users/vous/PTT Live/certs/localhost.pem
SSL_KEY=/Users/vous/PTT Live/certs/localhost-key.pem
NETWORK_IP=192.168.1.10 # Votre IP
```
**client/.env** :
```bash
VITE_SERVER_URL=https://192.168.1.10:3000
```
---
## ✅ Vérification
### 1. Certificats Créés ?
```bash
ls -lh certs/
# Doit afficher :
# localhost.pem
# localhost-key.pem
```
### 2. CA Installée ?
```bash
mkcert -CAROOT
# Affiche le chemin de la CA (ex: /Users/vous/Library/Application Support/mkcert)
```
### 3. Serveur HTTPS Fonctionne ?
```bash
# Démarrer
./start.sh --dev
# Vérifier
curl -k https://localhost:3000/health
# Doit retourner JSON sans erreur
```
### 4. Client HTTPS Fonctionne ?
Ouvrir dans navigateur :
```
https://localhost:5173
```
**Pas de warning SSL** = succès !
---
## 🐛 Dépannage
### Erreur "Certificats SSL introuvables"
**Symptôme** : Le serveur refuse de démarrer
**Solution** :
```bash
# 1. Vérifier que les certificats existent
ls certs/
# 2. Relancer le script
./setup-certificates.sh
# 3. Vérifier .env
cat server/.env | grep SSL_
```
### Warning SSL sur Smartphone
**Symptôme** : "Votre connexion n'est pas privée"
**Solution** : Normal ! Cliquer "Avancé" → "Continuer"
- Une seule fois par appareil
- La CA locale n'est pas sur le mobile
### Firefox : Certificat Non Approuvé
**Symptôme** : Firefox affiche warning (Chrome OK)
**Solution** :
```bash
# Installer NSS tools
brew install nss # macOS
sudo apt install libnss3-tools # Linux
# Réinstaller CA
mkcert -install
```
### Certificat Expiré
**Symptôme** : Après plusieurs mois
**Solution** :
```bash
# Régénérer certificats
cd certs
rm *.pem
mkcert localhost $(ipconfig getifaddr en0) *.local
# Redémarrer serveur
```
---
## 🔄 Renouvellement
Les certificats mkcert sont valides **10 ans** (pas besoin de renouveler).
Pour regénérer (changement d'IP, etc.) :
```bash
./setup-certificates.sh
# Écrase les anciens certificats
```
---
## 🌍 Production (Déploiement Réel)
### Option 1 : mkcert (Réseau Local Privé)
**Si PTT Live reste sur réseau local privé** (WiFi événement)
- Garder mkcert
- Les clients acceptent le certificat une fois
- Pas besoin de domaine/DNS
### Option 2 : Let's Encrypt (Internet Public)
⚠️ **Si PTT Live doit être accessible depuis Internet**
- Nécessite un domaine (ex: `ptt.votredomaine.com`)
- Utiliser Caddy ou Certbot (Let's Encrypt)
- Pas recommandé pour intercom événementiel
**Recommandation** : Rester sur **Option 1** (mkcert + réseau local)
---
## 📚 Ressources
- **mkcert** : https://github.com/FiloSottile/mkcert
- **Vite HTTPS** : https://vitejs.dev/config/server-options.html#server-https
- **Node.js HTTPS** : https://nodejs.org/api/https.html
---
## ✅ Récapitulatif
| Avant | Après |
|-------|-------|
| ❌ Certificats self-signed bloqués | ✅ Certificats approuvés automatiquement |
| ❌ Warnings "Non sécurisé" | ✅ Cadenas vert 🔒 |
| ❌ WebRTC refuse HTTPS invalide | ✅ WebRTC fonctionne |
| ❌ Configuration manuelle complexe | ✅ Script automatique 2 min |
| ❌ Dépendance cloud/domaine | ✅ 100% local |
---
**Solution : `./setup-certificates.sh` → 2 minutes → HTTPS fonctionnel**
🎉 Problème résolu définitivement !
+7 -7
View File
@@ -100,17 +100,17 @@ function App() {
const data = await response.json(); const data = await response.json();
// En mode dev (HTTPS via Vite), utiliser le proxy WebSocket // Si HTTPS, utiliser le proxy WebSocket (résout mixed content)
// En mode prod (HTTP direct), utiliser l'URL LiveKit directement // Sinon utiliser l'URL LiveKit directement
let livekitUrl = data.url; let livekitUrl = data.url;
if (import.meta.env.DEV && window.location.protocol === 'https:') { if (window.location.protocol === 'https:') {
// Mode dev avec Vite : utiliser le proxy WSS // HTTPS : utiliser le proxy WSS (wss://host:port/livekit)
livekitUrl = `${window.location.protocol}//${window.location.host}/livekit`; livekitUrl = `wss://${window.location.host}/livekit`;
console.log('🔒 Mode HTTPS : utilisation proxy WebSocket');
} }
console.log('🔗 Connexion LiveKit:', livekitUrl); console.log('🔗 Connexion LiveKit:', livekitUrl);
console.log('📝 Mode:', import.meta.env.DEV ? 'dev' : 'prod');
// Se connecter à LiveKit avec les canaux virtuels // Se connecter à LiveKit avec les canaux virtuels
await connect(livekitUrl, data.token, data.virtualChannels || []); await connect(livekitUrl, data.token, data.virtualChannels || []);
@@ -154,7 +154,7 @@ function App() {
// Adapter l'URL LiveKit selon le protocole de la page // Adapter l'URL LiveKit selon le protocole de la page
let livekitUrl = data.url; let livekitUrl = data.url;
if (window.location.protocol === 'https:') { if (window.location.protocol === 'https:') {
livekitUrl = `${window.location.protocol}//${window.location.host}/livekit`; livekitUrl = `wss://${window.location.host}/livekit`;
} }
// Changer de room LiveKit avec les canaux virtuels du nouveau groupe // Changer de room LiveKit avec les canaux virtuels du nouveau groupe
+19
View File
@@ -0,0 +1,19 @@
# Node
node_modules/
npm-debug.log
yarn-error.log
# Electron
dist/
out/
# OS
.DS_Store
Thumbs.db
# IDE
.vscode/
.idea/
# Logs
*.log
+153
View File
@@ -0,0 +1,153 @@
# PTT Live Desktop - Changelog
## v0.3.0 - 2026-06-19
### 🎉 Première version de l'application desktop Electron
#### ✨ Nouvelles Fonctionnalités
**Interface Electron**
- Application desktop native (macOS/Linux)
- Main Process spawn serveur Node.js automatiquement
- IPC sécurisé via contextBridge (preload.js)
- Démarrage/arrêt serveur depuis l'interface
- Tray icon placeholder (à compléter)
**Dashboard**
- Stats temps réel (uptime, utilisateurs, connexions)
- Liste utilisateurs connectés avec groupes
- Génération QR Code automatique (détection IP réseau)
- Bouton copier URL clients
- Polling automatique toutes les 2 secondes
**Configuration Audio**
- Sélection devices input/output (auto-détectés)
- Configuration sample rate (44.1/48/96 kHz)
- Bitrate par défaut (32-320 kbps)
- Jitter buffer (20-100 ms)
- Sauvegarde dans config.yaml
**Gestion Groupes**
- Liste groupes existants
- Création nouveau groupe (nom + bitrate)
- Modification/suppression (via API admin)
- Synchronisation config.yaml
**Monitoring**
- Logs serveur en temps réel
- Filtrage par niveau (error/warn/info/debug)
- Bouton effacer logs
- Format timestamp + niveau + message
**Notifications**
- Toast visuelles (success/error/warning/info)
- Auto-dismiss 5 secondes
- Bouton fermeture manuelle
- Animation slide-in
#### 🛠️ Technique
**Stack**
- Electron 28.0.0
- electron-builder 24.9.1
- qrcode 1.5.3 (via CDN)
- HTML/CSS/JS vanilla (pas de framework)
**Architecture**
- Main Process : spawn serveur, IPC handlers
- Renderer Process : dashboard, fetch API admin
- Communication : IPC + HTTP vers localhost:3000
**API Utilisées**
- `GET /admin/stats` : dashboard metrics
- `GET /admin/users` : utilisateurs
- `GET /admin/groups` : groupes
- `POST /admin/groups` : créer groupe
- `GET /admin/config` : config complète
- `PUT /admin/config/audio` : config audio
- `GET /admin/devices/list` : auto-détection devices
- `POST /admin/audio/device` : sélectionner device
- `GET /health` : health check
**Build**
- electron-builder configuré
- macOS : .dmg + .app
- Linux : .deb + .AppImage
- Scripts : `npm run build:mac` / `build:linux`
#### 📝 Documentation
- [DESKTOP-APP.md](DESKTOP-APP.md) : doc complète (architecture, API, debug)
- [QUICKSTART.md](QUICKSTART.md) : guide démarrage rapide
- [README.md](README.md) : intégration Electron dans README principal
- [CLAUDE.md](../CLAUDE.md) : section Application Desktop ajoutée
#### 🚧 TODO / Limitations
**À implémenter** :
- [ ] WebSocket audio levels (VU-mètres temps réel)
- [ ] Vraies icônes (icon.icns / icon.png)
- [ ] Tray icon fonctionnel avec menu
- [ ] Graphiques monitoring (Chart.js)
- [ ] Export logs (CSV/JSON)
- [ ] Matrice routing audio (drag & drop)
- [ ] Auth admin (mot de passe)
- [ ] Thème dark/light toggle
- [ ] Auto-update (electron-updater)
- [ ] Tests (Spectron/Playwright)
**Limitations connues** :
- QR Code utilise CDN (pas de lib locale)
- Pas de CSP (Content-Security-Policy)
- Pas de signature code (notarization macOS)
- Tray icon pas implémenté (commenté dans main.js)
#### 🔧 Installation
```bash
# Depuis la racine du projet
./start-desktop.sh
# OU depuis electron/
cd electron
npm install
npm start
```
#### 🏗️ Structure Fichiers
```
electron/
├── package.json # Config Electron
├── main.js # Main Process (585 lignes)
├── preload.js # IPC bridge (40 lignes)
├── README.md # Doc technique
├── QUICKSTART.md # Guide démarrage
├── CHANGELOG.md # Ce fichier
└── ui/
├── index.html # Interface (185 lignes)
├── styles.css # Styles (557 lignes)
└── app.js # Logic frontend (627 lignes)
```
---
## Prochaine version (v0.3.1)
### 🎯 Priorités
1. **VU-mètres WebSocket** : connexion `/audio-levels`
2. **Icônes** : créer icon.icns + icon.png + tray-icon.png
3. **Tray menu** : implémenter menu contextuel
4. **Tests** : premiers tests Electron
### 💡 Idées
- Graphiques latence/bande passante (Chart.js)
- Notifications desktop (Electron Notification API)
- Matrice routing visuelle
- Export config (JSON/YAML)
---
**Développé avec Claude Code**
+139
View File
@@ -0,0 +1,139 @@
# PTT Live Desktop - Quick Start Guide
## 🚀 Lancement en 30 secondes
```bash
# Depuis la racine du projet
./start-desktop.sh
```
C'est tout ! L'application démarre automatiquement le serveur.
---
## 📋 Checklist Première Utilisation
### 1. Vérifier le serveur
✅ Statut : **🟢 Actif** (coin haut-droit)
✅ Dashboard : stats doivent s'afficher sous 5s
### 2. Configurer l'audio
**Configuration → Périphériques Audio**
1. Sélectionner **Input Device** (carte son ou micro)
2. Sélectionner **Output Device** (haut-parleurs)
3. Cliquer **Appliquer**
💡 Les devices sont auto-détectés depuis votre système
### 3. Créer des groupes (optionnel)
**Groupes → Nouveau groupe**
1. Entrer un nom (ex: "Production")
2. Bitrate par défaut : 96 kbps (voix standard)
3. Sauvegarder
Les groupes sont enregistrés dans `server/config/config.yaml`
### 4. Connecter des clients
**Dashboard → QR Code**
1. Scanner le QR Code avec smartphone
2. OU copier l'URL et ouvrir dans navigateur
URL type : `https://192.168.1.10:5173`
---
## 🎯 Fonctionnalités Principales
### Dashboard
- **Stats** : uptime, utilisateurs, connexions
- **QR Code** : connexion rapide clients
- **Utilisateurs** : liste en temps réel
### Configuration
- **Audio** : devices, sample rate, bitrate, jitter buffer
- **Groupes** : créer/modifier/supprimer
### Monitoring
- **Logs** : serveur en temps réel, filtrables
---
## 🐛 Problèmes Courants
### Serveur ne démarre pas
**Symptôme** : statut reste "⚪ Arrêté"
**Solutions** :
1. Vérifier port 3000 libre :
```bash
lsof -i :3000
```
2. Vérifier LiveKit installé :
```bash
livekit-server --version
# OU
ls ../server/bin/livekit-server
```
3. Voir logs dans **Monitoring → Logs**
### QR Code ne s'affiche pas
**Symptôme** : zone blanche
**Solutions** :
1. Attendre 5-10s (génération après démarrage serveur)
2. Vérifier serveur actif (🟢)
3. Recharger : **Dashboard** → cliquer nav
### Pas d'audio
**Symptôme** : clients connectés mais pas de son
**Solutions** :
1. **Configuration** → vérifier devices sélectionnés
2. Vérifier permissions micro (système)
3. Tester avec devices différents
---
## ⌨️ Raccourcis
- `Cmd/Ctrl + R` : recharger interface
- `Cmd/Ctrl + Q` : quitter app
- `Cmd/Ctrl + Shift + I` : DevTools (debug)
---
## 📖 Documentation
- [DESKTOP-APP.md](DESKTOP-APP.md) : doc complète
- [README.md](../README.md) : vue d'ensemble projet
- [CLAUDE.md](../CLAUDE.md) : doc développement
---
## 🆘 Support
**Logs** : `Monitoring → Logs`
**DevTools** : `npm run dev` (dans terminal)
**Issues** : GitHub (si open source)
---
Bon intercom ! 🎙️
+171
View File
@@ -0,0 +1,171 @@
# PTT Live Desktop
Application desktop Electron pour gérer le serveur PTT Live.
## 🚀 Démarrage
```bash
# Depuis la racine du projet
./start-desktop.sh
# OU depuis electron/
cd electron
npm start
```
## 📦 Build pour distribution
```bash
cd electron
# macOS
npm run build:mac
# Linux
npm run build:linux
# Les deux
npm run build
```
Les builds seront dans `electron/dist/`.
## 🎨 Fonctionnalités
### Dashboard
- ✅ Stats temps réel (uptime, utilisateurs, connexions)
- ✅ Liste utilisateurs connectés
- ✅ QR Code pour connexion rapide clients
- ✅ Contrôles démarrage/arrêt serveur
### Configuration
- ✅ Sélection périphériques audio (input/output)
- ✅ Paramètres audio (sample rate, bitrate, jitter buffer)
- ✅ Sauvegarde automatique dans config.yaml
### Groupes
- ✅ Liste groupes configurés
- ✅ Ajout/modification/suppression groupes
- ✅ Configuration bitrate par groupe
### Monitoring
- 🚧 VU-mètres temps réel (WebSocket)
- 🚧 Graphiques latence
- 🚧 Stats réseau par client
### Logs
- ✅ Logs serveur en temps réel
- ✅ Filtrage par niveau (error/warn/info/debug)
- ✅ Export logs
## 🏗️ Architecture
```
electron/
├── main.js # Main Process (Node.js)
│ # - Spawn serveur PTT Live
│ # - IPC avec renderer
│ # - Gestion tray icon
├── preload.js # Bridge sécurisé IPC
└── ui/ # Renderer Process (Frontend)
├── index.html # Interface dashboard
├── styles.css # Styles
└── app.js # Logic frontend
# - Consomme API admin (/admin/*)
# - Met à jour UI
```
## 🔌 Communication
```
┌─────────────────────────────────────────┐
│ MAIN PROCESS (Node.js) │
│ ┌──────────────────────────────────┐ │
│ │ Serveur PTT Live (spawn) │ │
│ │ - LiveKit Server │ │
│ │ - Audio Bridge │ │
│ │ - API REST :3000 │ │
│ └──────────────────────────────────┘ │
│ ↕ IPC │
│ ┌──────────────────────────────────┐ │
│ │ Electron Window │ │
│ └──────────────────────────────────┘ │
└─────────────────────────────────────────┘
↕ HTTP
┌─────────────────────────────────────────┐
│ RENDERER PROCESS (Frontend) │
│ - Fetch API admin │
│ - WebSocket audio levels │
│ - Interface dashboard │
└─────────────────────────────────────────┘
```
## 🛠️ API Utilisées
Toutes les routes de l'API admin serveur :
```
GET /admin/stats → Dashboard metrics
GET /admin/users → Utilisateurs connectés
GET /admin/groups → Liste groupes
POST /admin/groups → Créer groupe
PUT /admin/groups/:id → Modifier groupe
DELETE /admin/groups/:id → Supprimer groupe
GET /admin/config → Config complète
PUT /admin/config/audio → Mettre à jour config audio
GET /admin/audio/devices → Énumérer devices
POST /admin/audio/device → Sélectionner device
GET /admin/audio/routing → Config routing
POST /admin/audio/routing → Mettre à jour routing
GET /admin/devices/list → Auto-détection devices
GET /admin/logs → Logs serveur
WS /audio-levels → WebSocket VU-mètres
```
## 🔧 TODO
- [ ] Implémenter QR Code canvas (bibliothèque qrcode.js)
- [ ] WebSocket audio levels pour VU-mètres
- [ ] Notifications desktop (toast)
- [ ] Tray icon avec vraie icône
- [ ] Graphiques monitoring (Chart.js)
- [ ] Export logs (CSV/JSON)
- [ ] Auth admin (optionnel)
- [ ] Thème dark/light toggle
- [ ] Auto-update (electron-updater)
## 📝 Notes de développement
- **Main Process** : Gère le cycle de vie de l'app et spawn le serveur
- **Renderer Process** : Interface web, appelle l'API REST du serveur
- **IPC** : Communication sécurisée via contextBridge
- **Serveur** : Tourne dans un process child_process, logs transmis au renderer
- **Port** : 3000 par défaut (configurable via PORT env)
## 🐛 Debug
Ouvrir DevTools : automatique en mode `--dev`
```bash
npm run dev
```
Logs dans la console :
- `[Serveur]` : logs du serveur PTT Live
- `[Serveur Error]` : erreurs serveur
- `✅/❌` : statut démarrage/arrêt
## 📦 Packaging
electron-builder crée :
- **macOS** : `.dmg` + `.app` dans `dist/mac/`
- **Linux** : `.deb` + `.AppImage` dans `dist/`
Tester le build :
```bash
npm run build:mac
open dist/mac/PTT\ Live\ Server.app
```
+580
View File
@@ -0,0 +1,580 @@
/**
* 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 fs = require('fs');
const { spawn } = require('child_process');
const http = require('http');
const https = require('https');
const QRCode = require('qrcode');
const yaml = require('yaml');
const setupHelper = require('./setup-helper');
const CONFIG_PATH = path.join(__dirname, '..', 'server', 'config', 'config.yaml');
function readConfig() {
return yaml.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
}
function writeConfig(config) {
fs.writeFileSync(CONFIG_PATH, yaml.stringify(config), 'utf8');
}
function slugify(text) {
return text.toString().normalize('NFD').replace(/[̀-ͯ]/g, '')
.toLowerCase().trim().replace(/\s+/g, '-').replace(/[^\w-]+/g, '').replace(/--+/g, '-');
}
// É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();
});
// ========== Groupes (lecture/écriture YAML directe, sans serveur) ==========
ipcMain.handle('groups:list', () => {
try {
const config = readConfig();
return { groups: config.groups || [] };
} catch (error) {
return { groups: [], error: error.message };
}
});
ipcMain.handle('groups:create', (event, { name, audioBitrate }) => {
try {
const config = readConfig();
const id = slugify(name);
if ((config.groups || []).find(g => slugify(g.name) === id)) {
return { success: false, error: `Un groupe "${name}" existe déjà` };
}
const group = { name, ...(audioBitrate ? { audioBitrate } : {}) };
config.groups = [...(config.groups || []), group];
writeConfig(config);
return { success: true, group };
} catch (error) {
return { success: false, error: error.message };
}
});
ipcMain.handle('groups:update', (event, { id, name, audioBitrate }) => {
try {
const config = readConfig();
const idx = (config.groups || []).findIndex(g => slugify(g.name) === id);
if (idx === -1) return { success: false, error: `Groupe ${id} introuvable` };
if (name !== undefined) config.groups[idx].name = name;
if (audioBitrate !== undefined) config.groups[idx].audioBitrate = audioBitrate;
writeConfig(config);
return { success: true, group: config.groups[idx] };
} catch (error) {
return { success: false, error: error.message };
}
});
ipcMain.handle('groups:delete', (event, { id }) => {
try {
const config = readConfig();
const idx = (config.groups || []).findIndex(g => slugify(g.name) === id);
if (idx === -1) return { success: false, error: `Groupe ${id} introuvable` };
config.groups.splice(idx, 1);
writeConfig(config);
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
});
ipcMain.handle('config:export', async () => {
const configPath = path.join(__dirname, '..', 'server', 'config', 'config.yaml');
try {
const content = fs.readFileSync(configPath, 'utf8');
const { filePath } = await dialog.showSaveDialog(mainWindow, {
title: 'Exporter la configuration',
defaultPath: 'config.yaml',
filters: [{ name: 'YAML', extensions: ['yaml', 'yml'] }]
});
if (!filePath) return { success: false, cancelled: true };
fs.writeFileSync(filePath, content, 'utf8');
return { success: true, filePath };
} catch (error) {
return { success: false, error: error.message };
}
});
ipcMain.handle('config:import', async () => {
const { filePaths } = await dialog.showOpenDialog(mainWindow, {
title: 'Importer une configuration',
filters: [{ name: 'YAML', extensions: ['yaml', 'yml'] }],
properties: ['openFile']
});
if (!filePaths || filePaths.length === 0) return { success: false, cancelled: true };
try {
const content = fs.readFileSync(filePaths[0], 'utf8');
const configPath = path.join(__dirname, '..', 'server', 'config', 'config.yaml');
// Backup de l'ancienne config avant remplacement
if (fs.existsSync(configPath)) {
fs.copyFileSync(configPath, configPath + '.bak');
}
fs.writeFileSync(configPath, content, 'utf8');
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
});
// 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);
});
+61
View File
@@ -0,0 +1,61 @@
{
"name": "ptt-live-desktop",
"version": "0.3.0",
"description": "PTT Live - Desktop Server Application",
"main": "main.js",
"scripts": {
"start": "electron .",
"dev": "electron . --dev",
"build": "electron-builder",
"build:mac": "electron-builder --mac",
"build:linux": "electron-builder --linux"
},
"build": {
"appId": "com.pttlive.desktop",
"productName": "PTT Live Server",
"directories": {
"output": "dist"
},
"files": [
"main.js",
"preload.js",
"ui/**/*",
"../server/**/*",
"!../server/node_modules",
"../server/node_modules/**/*"
],
"mac": {
"category": "public.app-category.utilities",
"icon": "assets/icon.icns",
"target": [
"dmg",
"zip"
]
},
"linux": {
"category": "AudioVideo",
"icon": "assets/icon.png",
"target": [
"deb",
"AppImage"
]
}
},
"keywords": [
"electron",
"webrtc",
"intercom",
"audio"
],
"author": "",
"license": "MIT",
"devDependencies": {
"electron": "^28.0.0",
"electron-builder": "^24.9.1"
},
"dependencies": {
"electron-store": "^8.1.0",
"qrcode": "^1.5.4",
"yaml": "^2.9.0"
}
}
+59
View File
@@ -0,0 +1,59 @@
/**
* PTT Live Desktop - Preload Script
* Bridge sécurisé entre Main Process et Renderer Process
*/
const { contextBridge, ipcRenderer } = require('electron');
// 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 ENABLE_HTTPS = process.env.ENABLE_HTTPS !== 'false';
const SERVER_URL = `${ENABLE_HTTPS ? 'https' : 'http'}://127.0.0.1:${SERVER_PORT}`;
// Exposer l'API au renderer de manière sécurisée
contextBridge.exposeInMainWorld('electronAPI', {
serverUrl: SERVER_URL,
// Contrôle serveur
server: {
start: () => ipcRenderer.invoke('server:start'),
stop: () => ipcRenderer.invoke('server:stop'),
status: () => ipcRenderer.invoke('server:status'),
ping: () => ipcRenderer.invoke('server:ping'),
// Écouter les événements du serveur
onStatus: (callback) => {
ipcRenderer.on('server:status', (event, data) => callback(data));
},
onLog: (callback) => {
ipcRenderer.on('server:log', (event, data) => callback(data));
}
},
// 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'),
// Export/import configuration YAML via dialog système
config: {
export: () => ipcRenderer.invoke('config:export'),
import: () => ipcRenderer.invoke('config:import')
},
// Groupes : lecture/écriture YAML directe (fonctionne sans serveur)
groups: {
list: () => ipcRenderer.invoke('groups:list'),
create: (data) => ipcRenderer.invoke('groups:create', data),
update: (data) => ipcRenderer.invoke('groups:update', data),
delete: (data) => ipcRenderer.invoke('groups:delete', data)
},
// Helpers
platform: process.platform,
version: process.env.npm_package_version || '0.3.0'
});
console.log('✅ Preload script chargé');
+249
View File
@@ -0,0 +1,249 @@
/**
* PTT Live Desktop - Setup Helper
* Automatise l'installation des dépendances et certificats
*/
const { exec } = require('child_process');
const { promisify } = require('util');
const { existsSync } = require('fs');
const { join } = require('path');
const os = require('os');
const execPromise = promisify(exec);
/**
* Vérifie si mkcert est installé
*/
async function isMkcertInstalled() {
try {
await execPromise('mkcert -version');
return true;
} catch (error) {
return false;
}
}
/**
* Installe mkcert automatiquement
*/
async function installMkcert() {
const platform = os.platform();
console.log('📦 Installation de mkcert...');
try {
if (platform === 'darwin') {
// macOS - via Homebrew
if (await isHomebrewInstalled()) {
await execPromise('brew install mkcert nss');
console.log('✅ mkcert installé via Homebrew');
return true;
} else {
throw new Error('Homebrew requis sur macOS');
}
} else if (platform === 'linux') {
// Linux - téléchargement direct
await execPromise('curl -JLO "https://dl.filippo.io/mkcert/latest?for=linux/amd64"');
await execPromise('chmod +x mkcert-v*-linux-amd64');
await execPromise('sudo mv mkcert-v*-linux-amd64 /usr/local/bin/mkcert');
console.log('✅ mkcert installé');
return true;
} else {
throw new Error(`Plateforme non supportée: ${platform}`);
}
} catch (error) {
console.error('❌ Erreur installation mkcert:', error.message);
return false;
}
}
/**
* Vérifie si Homebrew est installé
*/
async function isHomebrewInstalled() {
try {
await execPromise('brew --version');
return true;
} catch (error) {
return false;
}
}
/**
* Installe la CA locale
*/
async function installCA() {
try {
console.log('🔑 Installation de la Certificate Authority locale...');
await execPromise('mkcert -install');
console.log('✅ CA locale installée');
return true;
} catch (error) {
console.error('❌ Erreur installation CA:', error.message);
return false;
}
}
/**
* Détecte l'IP réseau locale
*/
function getNetworkIP() {
const interfaces = os.networkInterfaces();
// Priorité : WiFi > Ethernet
const priority = ['en0', 'en1', 'eth0', 'wlan0'];
for (const name of priority) {
const iface = interfaces[name];
if (iface) {
for (const net of iface) {
if (net.family === 'IPv4' && !net.internal) {
return net.address;
}
}
}
}
// Fallback : première IP non-interne
for (const name of Object.keys(interfaces)) {
for (const net of interfaces[name]) {
if (net.family === 'IPv4' && !net.internal) {
return net.address;
}
}
}
return '192.168.1.100'; // Fallback ultime
}
/**
* Génère les certificats SSL
*/
async function generateCertificates(certsDir) {
try {
const networkIP = getNetworkIP();
const hostname = os.hostname();
console.log('📜 Génération des certificats...');
console.log(` IP réseau : ${networkIP}`);
// Créer répertoire si nécessaire
if (!existsSync(certsDir)) {
await execPromise(`mkdir -p "${certsDir}"`);
}
// Générer certificats
const cmd = `cd "${certsDir}" && mkcert localhost 127.0.0.1 ::1 "${networkIP}" "*.local" "${hostname}.local"`;
await execPromise(cmd);
// Renommer pour simplifier
const files = await execPromise(`ls "${certsDir}"/*.pem`);
const fileList = files.stdout.trim().split('\n');
// Trouver les fichiers générés
const certFile = fileList.find(f => !f.includes('-key.pem'));
const keyFile = fileList.find(f => f.includes('-key.pem'));
if (certFile && keyFile) {
// Copier avec noms standards
await execPromise(`cp "${certFile}" "${join(certsDir, 'localhost.pem')}"`);
await execPromise(`cp "${keyFile}" "${join(certsDir, 'localhost-key.pem')}"`);
}
console.log('✅ Certificats générés');
return { networkIP, certPath: join(certsDir, 'localhost.pem'), keyPath: join(certsDir, 'localhost-key.pem') };
} catch (error) {
console.error('❌ Erreur génération certificats:', error.message);
return null;
}
}
/**
* Vérifie si les certificats existent et sont valides
*/
function certificatesExist(certsDir) {
const certPath = join(certsDir, 'localhost.pem');
const keyPath = join(certsDir, 'localhost-key.pem');
return existsSync(certPath) && existsSync(keyPath);
}
/**
* Setup complet automatique
*/
async function autoSetup(projectRoot) {
const certsDir = join(projectRoot, 'certs');
console.log('🚀 Configuration automatique PTT Live...\n');
// 1. Vérifier certificats existants
if (certificatesExist(certsDir)) {
console.log('✅ Certificats déjà présents');
return { success: true, needsRestart: false };
}
console.log('⚠️ Certificats SSL non trouvés\n');
// 2. Vérifier mkcert
const hasMkcert = await isMkcertInstalled();
if (!hasMkcert) {
console.log('📦 mkcert non installé, installation...\n');
const installed = await installMkcert();
if (!installed) {
return {
success: false,
error: 'Installation mkcert échouée',
manual: true,
instructions: 'Installez mkcert manuellement : https://github.com/FiloSottile/mkcert'
};
}
} else {
console.log('✅ mkcert déjà installé\n');
}
// 3. Installer CA locale
const caInstalled = await installCA();
if (!caInstalled) {
return {
success: false,
error: 'Installation CA échouée',
manual: true
};
}
console.log('');
// 4. Générer certificats
const result = await generateCertificates(certsDir);
if (!result) {
return {
success: false,
error: 'Génération certificats échouée',
manual: true
};
}
console.log('\n✅ Configuration terminée !');
console.log(` Certificats : ${certsDir}`);
console.log(` IP réseau : ${result.networkIP}\n`);
return {
success: true,
needsRestart: false,
networkIP: result.networkIP,
certPath: result.certPath,
keyPath: result.keyPath
};
}
module.exports = {
isMkcertInstalled,
installMkcert,
installCA,
generateCertificates,
certificatesExist,
getNetworkIP,
autoSetup
};
+1103
View File
File diff suppressed because it is too large Load Diff
+216
View File
@@ -0,0 +1,216 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PTT Live Server</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<!-- Toast Container -->
<div class="toast-container" id="toast-container"></div>
<div id="app">
<!-- Header -->
<header class="header">
<div class="header-left">
<h1>🎙️ PTT Live Server</h1>
<span class="version" id="version">v0.3.0</span>
</div>
<div class="header-right">
<div class="server-status">
<span class="status-indicator" id="status-indicator"></span>
<span id="status-text">Arrêté</span>
</div>
<button id="btn-start" class="btn btn-primary">Démarrer</button>
<button id="btn-stop" class="btn btn-secondary" disabled>Arrêter</button>
</div>
</header>
<!-- Main Content -->
<main class="main-content">
<!-- Sidebar Navigation -->
<nav class="sidebar">
<button class="nav-item active" data-view="dashboard">
📊 Dashboard
</button>
<button class="nav-item" data-view="config">
⚙️ Configuration
</button>
<button class="nav-item" data-view="groups">
👥 Groupes
</button>
<button class="nav-item" data-view="monitoring">
📈 Monitoring
</button>
<button class="nav-item" data-view="logs">
📝 Logs
</button>
</nav>
<!-- Content Area -->
<div class="content">
<!-- Dashboard View -->
<div id="view-dashboard" class="view active">
<h2>Dashboard</h2>
<!-- Stats Cards -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">Uptime</div>
<div class="stat-value" id="stat-uptime">--</div>
</div>
<div class="stat-card">
<div class="stat-label">Utilisateurs</div>
<div class="stat-value" id="stat-users">--</div>
</div>
<div class="stat-card">
<div class="stat-label">Groupes actifs</div>
<div class="stat-value" id="stat-groups">--</div>
</div>
<div class="stat-card">
<div class="stat-label">Connexions totales</div>
<div class="stat-value" id="stat-total-connections">--</div>
</div>
</div>
<!-- QR Code Section -->
<div class="section">
<h3>📱 Connexion rapide clients</h3>
<div class="qr-container">
<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">
<p><strong>URL clients :</strong></p>
<p class="url-text" id="client-url">--</p>
<button class="btn btn-small" id="btn-copy-url">Copier l'URL</button>
</div>
</div>
</div>
<!-- Active Users -->
<div class="section">
<h3>👤 Utilisateurs connectés</h3>
<div id="users-list" class="users-list">
<p class="empty-state">Aucun utilisateur connecté</p>
</div>
</div>
</div>
<!-- Configuration View -->
<div id="view-config" class="view">
<h2>Configuration Audio</h2>
<div class="section">
<h3>🔌 Périphériques Audio</h3>
<div class="form-group">
<label>Device Input</label>
<select id="input-device" class="form-control">
<option>Chargement...</option>
</select>
</div>
<div class="form-group">
<label>Device Output</label>
<select id="output-device" class="form-control">
<option>Chargement...</option>
</select>
</div>
<button class="btn btn-primary" id="btn-save-device">Appliquer</button>
</div>
<div class="section">
<h3>💾 Sauvegarde de configuration</h3>
<div class="config-actions">
<button class="btn btn-secondary" id="btn-export-config">Exporter config.yaml</button>
<button class="btn btn-secondary" id="btn-import-config">Importer config.yaml</button>
</div>
<p class="config-note">L'import remplace config.yaml (backup automatique en .bak). Redémarrez le serveur pour appliquer.</p>
</div>
<div class="section">
<h3>🎚️ Paramètres Audio</h3>
<div class="form-group">
<label>Sample Rate</label>
<select id="sample-rate" class="form-control">
<option value="44100">44.1 kHz</option>
<option value="48000" selected>48 kHz</option>
<option value="96000">96 kHz</option>
</select>
</div>
<div class="form-group">
<label>Bitrate par défaut (kbps)</label>
<input type="number" id="default-bitrate" class="form-control" value="96" min="32" max="320" step="32">
</div>
<div class="form-group">
<label>Jitter Buffer (ms)</label>
<input type="number" id="jitter-buffer" class="form-control" value="40" min="20" max="100" step="10">
</div>
<button class="btn btn-primary" id="btn-save-audio">Sauvegarder</button>
</div>
</div>
<!-- Groups View -->
<div id="view-groups" class="view">
<h2>Gestion des Groupes</h2>
<button class="btn btn-primary" id="btn-add-group"> Nouveau groupe</button>
<div id="groups-list" class="groups-list">
<p class="empty-state">Chargement des groupes...</p>
</div>
</div>
<!-- Monitoring View -->
<div id="view-monitoring" class="view">
<h2>Monitoring Audio</h2>
<div class="section">
<h3>🔊 VU-Mètres</h3>
<div id="vu-meters" class="vu-meters">
<p class="empty-state">En attente de données audio...</p>
</div>
</div>
</div>
<!-- Logs View -->
<div id="view-logs" class="view">
<h2>Logs Serveur</h2>
<div class="logs-controls">
<button class="btn btn-small" id="btn-clear-logs">Effacer</button>
<button class="btn btn-small btn-secondary" id="btn-export-logs">Exporter JSON</button>
<select id="log-level-filter" class="form-control form-control-small">
<option value="">Tous les niveaux</option>
<option value="error">Erreurs</option>
<option value="warn">Warnings</option>
<option value="info">Info</option>
<option value="debug">Debug</option>
</select>
</div>
<div id="logs-container" class="logs-container">
<p class="empty-state">Aucun log</p>
</div>
</div>
</div>
</main>
</div>
<!-- Modal générique -->
<div id="modal-overlay" class="modal-overlay hidden">
<div class="modal">
<div class="modal-header">
<h3 id="modal-title"></h3>
</div>
<div class="modal-body" id="modal-body"></div>
<div class="modal-footer">
<button class="btn btn-secondary" id="modal-cancel">Annuler</button>
<button class="btn btn-primary" id="modal-confirm">Confirmer</button>
</div>
</div>
</div>
<!-- Le QR Code est généré côté Main Process (lib qrcode Node), pas de dépendance CDN -->
<script src="app.js"></script>
</body>
</html>
+802
View File
@@ -0,0 +1,802 @@
/**
* PTT Live Desktop - Styles
*/
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg-primary: #1a1a1a;
--bg-secondary: #2a2a2a;
--bg-tertiary: #3a3a3a;
--text-primary: #ffffff;
--text-secondary: #b0b0b0;
--accent-primary: #4a9eff;
--accent-success: #4caf50;
--accent-warning: #ff9800;
--accent-error: #f44336;
--border-color: #444;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
overflow: hidden;
}
#app {
display: flex;
flex-direction: column;
height: 100vh;
}
/* Header */
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
}
.header-left {
display: flex;
align-items: center;
gap: 1rem;
}
.header h1 {
font-size: 1.5rem;
font-weight: 600;
}
.version {
font-size: 0.875rem;
color: var(--text-secondary);
background: var(--bg-tertiary);
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
.header-right {
display: flex;
align-items: center;
gap: 1rem;
}
.server-status {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: var(--bg-tertiary);
border-radius: 6px;
}
.status-indicator {
font-size: 1.25rem;
}
/* Buttons */
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-primary {
background: var(--accent-primary);
color: white;
}
.btn-primary:hover:not(:disabled) {
background: #3d8eef;
}
.btn-secondary {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.btn-secondary:hover:not(:disabled) {
background: #4a4a4a;
}
.btn-small {
padding: 0.375rem 0.75rem;
font-size: 0.8125rem;
}
.btn-danger {
background: var(--accent-error);
color: white;
}
.btn-danger:hover:not(:disabled) {
background: #d32f2f;
}
/* Main Content */
.main-content {
display: flex;
flex: 1;
overflow: hidden;
}
/* Sidebar */
.sidebar {
width: 200px;
background: var(--bg-secondary);
border-right: 1px solid var(--border-color);
padding: 1rem 0;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.nav-item {
padding: 0.75rem 1.5rem;
background: none;
border: none;
color: var(--text-secondary);
text-align: left;
font-size: 0.9375rem;
cursor: pointer;
transition: all 0.2s;
border-left: 3px solid transparent;
}
.nav-item:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.nav-item.active {
background: var(--bg-tertiary);
color: var(--text-primary);
border-left-color: var(--accent-primary);
}
/* Content */
.content {
flex: 1;
overflow-y: auto;
padding: 2rem;
}
.view {
display: none;
}
.view.active {
display: block;
}
.view h2 {
font-size: 1.75rem;
margin-bottom: 1.5rem;
}
.view h3 {
font-size: 1.25rem;
margin-bottom: 1rem;
}
/* Stats Grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.stat-card {
background: var(--bg-secondary);
padding: 1.5rem;
border-radius: 8px;
border: 1px solid var(--border-color);
}
.stat-label {
font-size: 0.875rem;
color: var(--text-secondary);
margin-bottom: 0.5rem;
}
.stat-value {
font-size: 2rem;
font-weight: 600;
color: var(--accent-primary);
}
/* Sections */
.section {
background: var(--bg-secondary);
padding: 1.5rem;
border-radius: 8px;
border: 1px solid var(--border-color);
margin-bottom: 1.5rem;
}
/* QR Code */
.qr-container {
display: flex;
gap: 2rem;
align-items: center;
}
.qr-wrapper {
position: relative;
width: 256px;
height: 256px;
flex-shrink: 0;
}
#qr-code {
display: none;
width: 256px;
height: 256px;
border: 4px solid white;
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 {
flex: 1;
}
.url-text {
font-family: 'Courier New', monospace;
background: var(--bg-tertiary);
padding: 0.75rem;
border-radius: 6px;
margin: 0.5rem 0 1rem 0;
word-break: break-all;
}
/* Users List */
.users-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.user-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
background: var(--bg-tertiary);
border-radius: 6px;
}
.user-info {
display: flex;
align-items: center;
gap: 1rem;
}
.user-status {
font-size: 1.25rem;
}
.user-details h4 {
font-size: 1rem;
margin-bottom: 0.25rem;
}
.user-details p {
font-size: 0.875rem;
color: var(--text-secondary);
}
.user-badge {
padding: 0.25rem 0.75rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
background: var(--bg-primary);
}
.user-badge.ptt-active {
background: var(--accent-error);
}
/* Forms */
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 0.5rem;
color: var(--text-secondary);
}
.form-control {
width: 100%;
padding: 0.75rem;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-primary);
font-size: 0.9375rem;
}
.form-control-small {
width: auto;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
}
.form-control:focus {
outline: none;
border-color: var(--accent-primary);
}
/* Modal */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
}
.modal-overlay.hidden {
display: none;
}
.modal {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 10px;
width: 420px;
max-width: 90vw;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
}
.modal-header {
padding: 1.25rem 1.5rem;
border-bottom: 1px solid var(--border-color);
}
.modal-header h3 {
font-size: 1.125rem;
margin: 0;
}
.modal-body {
padding: 1.5rem;
}
.modal-message {
color: var(--text-primary);
line-height: 1.5;
margin: 0;
}
.modal-footer {
padding: 1rem 1.5rem;
border-top: 1px solid var(--border-color);
display: flex;
justify-content: flex-end;
gap: 0.75rem;
}
/* Config actions */
.config-actions {
display: flex;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.config-note {
font-size: 0.8125rem;
color: var(--text-secondary);
}
/* Groups List */
.groups-list {
display: flex;
flex-direction: column;
gap: 1rem;
margin-top: 1rem;
}
.group-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.5rem;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
}
.group-info h4 {
font-size: 1.125rem;
margin-bottom: 0.5rem;
}
.group-info p {
font-size: 0.875rem;
color: var(--text-secondary);
}
.group-actions {
display: flex;
gap: 0.5rem;
}
/* VU Meters */
.vu-meters {
display: flex;
flex-direction: column;
gap: 1rem;
}
.vu-meter {
display: flex;
align-items: center;
gap: 1rem;
}
.vu-label {
width: 120px;
font-size: 0.875rem;
color: var(--text-secondary);
}
.vu-bar {
flex: 1;
height: 24px;
background: var(--bg-tertiary);
border-radius: 4px;
overflow: hidden;
position: relative;
}
.vu-fill {
height: 100%;
background: linear-gradient(90deg, var(--accent-success), var(--accent-warning), var(--accent-error));
transition: width 0.1s ease-out;
}
.vu-value {
width: 60px;
text-align: right;
font-size: 0.875rem;
font-weight: 500;
}
/* Logs */
.logs-controls {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
}
.logs-container {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1rem;
max-height: 600px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 0.8125rem;
}
.log-entry {
padding: 0.5rem;
border-bottom: 1px solid var(--border-color);
display: flex;
gap: 1rem;
}
.log-entry:last-child {
border-bottom: none;
}
.log-time {
color: var(--text-secondary);
white-space: nowrap;
}
.log-level {
width: 60px;
font-weight: 600;
text-transform: uppercase;
}
.log-level.error { color: var(--accent-error); }
.log-level.warn { color: var(--accent-warning); }
.log-level.info { color: var(--accent-primary); }
.log-level.debug { color: var(--text-secondary); }
.log-message {
flex: 1;
word-break: break-word;
}
/* Empty State */
.empty-state {
text-align: center;
color: var(--text-secondary);
padding: 2rem;
font-size: 0.9375rem;
}
/* Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-primary);
}
::-webkit-scrollbar-thumb {
background: var(--bg-tertiary);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #4a4a4a;
}
/* Toast Notifications */
.toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 10px;
}
.toast {
min-width: 300px;
padding: 1rem 1.5rem;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
gap: 0.75rem;
animation: slideIn 0.3s ease-out;
}
.toast.success {
border-left: 4px solid var(--accent-success);
}
.toast.error {
border-left: 4px solid var(--accent-error);
}
.toast.warning {
border-left: 4px solid var(--accent-warning);
}
.toast.info {
border-left: 4px solid var(--accent-primary);
}
.toast-icon {
font-size: 1.5rem;
}
.toast-message {
flex: 1;
font-size: 0.9375rem;
}
.toast-close {
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
font-size: 1.25rem;
padding: 0;
transition: color 0.2s;
}
.toast-close:hover {
color: var(--text-primary);
}
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* VU Meters */
.vu-meters {
padding: 1rem 0;
}
.vu-status {
font-size: 0.875rem;
padding: 0.5rem;
margin-bottom: 1rem;
border-radius: 4px;
background: var(--bg-tertiary);
text-align: center;
}
.vu-status.connected {
background: rgba(76, 175, 80, 0.2);
color: var(--accent-success);
}
.vu-status.disconnected {
background: rgba(244, 67, 54, 0.2);
color: var(--accent-error);
}
.vu-section {
margin-bottom: 2rem;
}
.vu-section h4 {
font-size: 0.875rem;
color: var(--text-secondary);
margin-bottom: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.vu-grid {
display: grid;
gap: 0.75rem;
}
.vu-meter {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 0.75rem;
transition: border-color 0.2s;
}
.vu-meter-clipping {
border-color: var(--accent-error);
animation: pulseClipping 0.5s ease-in-out infinite;
}
@keyframes pulseClipping {
0%, 100% {
border-color: var(--accent-error);
}
50% {
border-color: transparent;
}
}
.vu-label {
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 0.5rem;
color: var(--text-primary);
}
.vu-bar-container {
position: relative;
height: 24px;
background: var(--bg-tertiary);
border-radius: 3px;
overflow: hidden;
margin-bottom: 0.5rem;
}
.vu-bar {
height: 100%;
transition: width 0.05s linear;
border-radius: 3px;
}
.vu-bar-green {
background: linear-gradient(to right, #4caf50, #66bb6a);
}
.vu-bar-yellow {
background: linear-gradient(to right, #ff9800, #ffa726);
}
.vu-bar-red {
background: linear-gradient(to right, #f44336, #e57373);
}
.vu-peak {
position: absolute;
top: 0;
width: 2px;
height: 100%;
background: #ffffff;
box-shadow: 0 0 4px rgba(255, 255, 255, 0.8);
transition: left 0.1s linear;
}
.vu-values {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.75rem;
}
.vu-rms {
color: var(--text-secondary);
}
.vu-clip {
color: var(--accent-error);
font-weight: bold;
animation: blinkClip 0.5s ease-in-out infinite;
}
@keyframes blinkClip {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.3;
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ audio:
sampleRate: 48000 sampleRate: 48000
channels: 2 channels: 2
frameSize: 20 frameSize: 20
defaultBitrate: 96 defaultBitrate: 128
jitterBufferMs: 40 jitterBufferMs: 40
device: device:
inputDeviceId: Loopback Audio 4 inputDeviceId: Loopback Audio 4
+57 -4
View File
@@ -16,6 +16,7 @@ import configManager from './config/ConfigManager.js';
import audioBridgeManager from './bridge/AudioBridgeManager.js'; import audioBridgeManager from './bridge/AudioBridgeManager.js';
import AudioLevelsServer from './websocket/AudioLevelsServer.js'; import AudioLevelsServer from './websocket/AudioLevelsServer.js';
import { setGlobalLogLevel } from './utils/Logger.js'; import { setGlobalLogLevel } from './utils/Logger.js';
import httpProxy from 'http-proxy';
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -378,6 +379,33 @@ apiRouter.get('/health', (req, res) => {
}); });
}); });
// Créer proxy WebSocket natif pour LiveKit (wss → ws)
const livekitProxy = httpProxy.createProxyServer({
target: 'http://localhost:7880',
ws: true,
changeOrigin: true
});
livekitProxy.on('error', (err, req, res) => {
log('error', `❌ Erreur proxy LiveKit: ${err.message}`);
if (res && res.writeHead) {
res.writeHead(502, { 'Content-Type': 'text/plain' });
res.end('Proxy error');
}
});
livekitProxy.on('proxyReqWs', (proxyReq, req, socket, options, head) => {
log('debug', `🔀 Proxy WebSocket: ${req.url} → ws://localhost:7880`);
});
// Proxy HTTP pour LiveKit (requêtes REST comme /rtc/validate)
app.use('/livekit', (req, res) => {
log('debug', `🔀 Proxy HTTP: ${req.originalUrl} → http://localhost:7880${req.url}`);
livekitProxy.web(req, res, {
target: 'http://localhost:7880'
});
});
// Monter le router API sous /api ET à la racine (rétrocompatibilité) // Monter le router API sous /api ET à la racine (rétrocompatibilité)
app.use('/api', apiRouter); app.use('/api', apiRouter);
app.use(apiRouter); // Routes accessibles aussi sans préfixe /api app.use(apiRouter); // Routes accessibles aussi sans préfixe /api
@@ -437,11 +465,19 @@ async function start() {
let server; let server;
if (ENABLE_HTTPS) { if (ENABLE_HTTPS) {
// Charger certificats SSL (mêmes que Vite) // Charger certificats SSL depuis .env ou fallback
const certPath = join(__dirname, '..', 'client'); const certPath = process.env.SSL_CERT || join(__dirname, '..', 'certs', 'localhost.pem');
const keyPath = process.env.SSL_KEY || join(__dirname, '..', 'certs', 'localhost-key.pem');
if (!existsSync(certPath) || !existsSync(keyPath)) {
log('error', '❌ Certificats SSL introuvables');
log('info', '💡 Exécutez : ./setup-certificates.sh');
process.exit(1);
}
const httpsOptions = { const httpsOptions = {
key: readFileSync(join(certPath, 'localhost+3-key.pem')), key: readFileSync(keyPath),
cert: readFileSync(join(certPath, 'localhost+3.pem')) cert: readFileSync(certPath)
}; };
server = https.createServer(httpsOptions, app); server = https.createServer(httpsOptions, app);
@@ -485,8 +521,25 @@ async function start() {
} }
// 2.5 Démarrer WebSocket Audio Levels (même port que l'API) // 2.5 Démarrer WebSocket Audio Levels (même port que l'API)
// noServer: true en interne, l'upgrade est dispatché ci-dessous
const audioLevelsServer = new AudioLevelsServer({ server }); const audioLevelsServer = new AudioLevelsServer({ server });
audioLevelsServer.start(); audioLevelsServer.start();
// 2.6 Dispatcher unique pour les upgrades WebSocket du port HTTP/HTTPS
// (proxy LiveKit et audio-levels partagent le même serveur, donc le même
// événement 'upgrade' : un seul listener doit trancher par chemin)
server.on('upgrade', (req, socket, head) => {
if (req.url.startsWith('/livekit')) {
req.url = req.url.replace(/^\/livekit/, '');
livekitProxy.ws(req, socket, head);
} else if (req.url.startsWith('/audio-levels')) {
audioLevelsServer.handleUpgrade(req, socket, head);
} else {
log('warn', `⚠️ Unknown WebSocket path: ${req.url}`);
socket.destroy();
}
});
const wsProtocol = ENABLE_HTTPS ? 'wss' : 'ws'; const wsProtocol = ENABLE_HTTPS ? 'wss' : 'ws';
log('info', `✓ WebSocket Audio Levels démarré sur ${wsProtocol}://${SERVER_HOST}:${SERVER_PORT}`); log('info', `✓ WebSocket Audio Levels démarré sur ${wsProtocol}://${SERVER_HOST}:${SERVER_PORT}`);
+2
View File
@@ -22,6 +22,8 @@
"@livekit/rtc-node": "^0.13.28", "@livekit/rtc-node": "^0.13.28",
"dotenv": "^17.4.2", "dotenv": "^17.4.2",
"express": "^4.19.2", "express": "^4.19.2",
"http-proxy": "^1.18.1",
"http-proxy-middleware": "^4.1.1",
"livekit-server-sdk": "^2.6.0", "livekit-server-sdk": "^2.6.0",
"opusscript": "^0.1.1", "opusscript": "^0.1.1",
"qrcode-terminal": "^0.12.0", "qrcode-terminal": "^0.12.0",
+13 -1
View File
@@ -91,9 +91,11 @@ export class AudioLevelsServer extends EventEmitter {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
// Si un serveur HTTP est fourni, utiliser le même port (upgrade HTTP → WebSocket) // Si un serveur HTTP est fourni, utiliser le même port (upgrade HTTP → WebSocket)
// noServer: true car l'upgrade est dispatché manuellement par server/index.js
// (un seul listener 'upgrade' partagé avec le proxy LiveKit, voir handleUpgrade())
// Sinon, créer un serveur WebSocket standalone sur son propre port // Sinon, créer un serveur WebSocket standalone sur son propre port
const wsOptions = this.options.server const wsOptions = this.options.server
? { server: this.options.server, path: '/audio-levels' } ? { noServer: true }
: { port: this.options.port }; : { port: this.options.port };
this.wss = new WebSocketServer(wsOptions); this.wss = new WebSocketServer(wsOptions);
@@ -125,6 +127,16 @@ export class AudioLevelsServer extends EventEmitter {
}); });
} }
/**
* Complète l'upgrade WebSocket pour une requête déjà identifiée comme
* ciblant ce serveur (voir le dispatcher 'upgrade' dans server/index.js)
*/
handleUpgrade(req, socket, head) {
this.wss.handleUpgrade(req, socket, head, (ws) => {
this.wss.emit('connection', ws, req);
});
}
/** /**
* Gère une nouvelle connexion client * Gère une nouvelle connexion client
*/ */
+324
View File
@@ -0,0 +1,324 @@
#!/bin/bash
# PTT Live - Configuration Certificats SSL Locaux
# Génère des certificats auto-signés DE CONFIANCE pour développement local
set -e
echo "🔐 Configuration Certificats SSL Locaux PTT Live"
echo ""
# Détection OS
OS="$(uname -s)"
# Couleurs
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# ========== Installation mkcert ==========
echo "📦 Vérification mkcert..."
if ! command -v mkcert &> /dev/null; then
echo -e "${YELLOW}⚠️ mkcert non installé${NC}"
echo ""
echo "Installation de mkcert (génère certificats de confiance)..."
echo ""
if [[ "$OS" == "Darwin" ]]; then
# macOS
if command -v brew &> /dev/null; then
brew install mkcert
brew install nss # Pour Firefox
else
echo -e "${RED}❌ Homebrew requis sur macOS${NC}"
echo "Installez Homebrew : https://brew.sh"
exit 1
fi
elif [[ "$OS" == "Linux" ]]; then
# Linux
if command -v apt-get &> /dev/null; then
# Debian/Ubuntu
sudo apt-get update
sudo apt-get install -y libnss3-tools
# Télécharger mkcert
curl -JLO "https://dl.filippo.io/mkcert/latest?for=linux/amd64"
chmod +x mkcert-v*-linux-amd64
sudo mv mkcert-v*-linux-amd64 /usr/local/bin/mkcert
elif command -v yum &> /dev/null; then
# RedHat/CentOS
sudo yum install -y nss-tools
curl -JLO "https://dl.filippo.io/mkcert/latest?for=linux/amd64"
chmod +x mkcert-v*-linux-amd64
sudo mv mkcert-v*-linux-amd64 /usr/local/bin/mkcert
else
echo -e "${RED}❌ Gestionnaire de paquets non supporté${NC}"
echo "Installez mkcert manuellement : https://github.com/FiloSottile/mkcert"
exit 1
fi
else
echo -e "${RED}❌ OS non supporté : $OS${NC}"
exit 1
fi
echo -e "${GREEN}✅ mkcert installé${NC}"
echo ""
else
echo -e "${GREEN}✅ mkcert déjà installé${NC}"
echo ""
fi
# ========== Installation CA Locale ==========
echo "🔑 Installation Certificate Authority (CA) locale..."
echo ""
echo "⚠️ Ceci va ajouter une CA locale au système"
echo " Les certificats générés seront automatiquement approuvés"
echo ""
# Installer la CA locale (une seule fois par machine)
mkcert -install
if [ $? -eq 0 ]; then
echo -e "${GREEN}✅ CA locale installée${NC}"
else
echo -e "${RED}❌ Erreur installation CA${NC}"
exit 1
fi
echo ""
# ========== Génération Certificats ==========
echo "📜 Génération certificats pour PTT Live..."
echo ""
# Détecter l'IP réseau
if [[ "$OS" == "Darwin" ]]; then
# macOS
NETWORK_IP=$(ipconfig getifaddr en0 || ipconfig getifaddr en1 || echo "192.168.1.100")
elif [[ "$OS" == "Linux" ]]; then
# Linux
NETWORK_IP=$(ip route get 1 | awk '{print $7; exit}' || echo "192.168.1.100")
fi
echo "🌐 IP réseau détectée : $NETWORK_IP"
echo ""
# Créer répertoire certificats
CERT_DIR="$(pwd)/certs"
mkdir -p "$CERT_DIR"
cd "$CERT_DIR"
# Générer certificats pour :
# - localhost
# - IP réseau locale
# - *.local (wildcard)
echo "Génération certificats pour :"
echo " - localhost"
echo " - $NETWORK_IP"
echo " - *.local"
echo ""
mkcert \
localhost \
127.0.0.1 \
::1 \
"$NETWORK_IP" \
"*.local" \
"$(hostname).local"
if [ $? -eq 0 ]; then
echo ""
echo -e "${GREEN}✅ Certificats générés dans : $CERT_DIR${NC}"
echo ""
# Renommer pour simplifier
mv localhost+*.pem localhost.pem 2>/dev/null || true
mv localhost+*-key.pem localhost-key.pem 2>/dev/null || true
echo "📁 Fichiers créés :"
ls -lh "$CERT_DIR"/*.pem
else
echo -e "${RED}❌ Erreur génération certificats${NC}"
exit 1
fi
echo ""
# ========== Configuration Serveur ==========
echo "⚙️ Configuration automatique du serveur..."
echo ""
# Créer/mettre à jour .env serveur
SERVER_ENV="$(pwd)/../server/.env"
if [ -f "$SERVER_ENV" ]; then
# Backup
cp "$SERVER_ENV" "$SERVER_ENV.backup"
echo "💾 Backup : $SERVER_ENV.backup"
fi
# Détecter les fichiers de certificats générés
CERT_FILE=$(ls "$CERT_DIR"/localhost.pem 2>/dev/null || ls "$CERT_DIR"/*+*.pem | head -1)
KEY_FILE=$(ls "$CERT_DIR"/localhost-key.pem 2>/dev/null || ls "$CERT_DIR"/*-key.pem | head -1)
if [ -z "$CERT_FILE" ] || [ -z "$KEY_FILE" ]; then
echo -e "${RED}❌ Certificats introuvables${NC}"
exit 1
fi
# Mettre à jour .env avec chemins absolus
cat > "$SERVER_ENV" << EOF
# PTT Live Server - Configuration
# Généré automatiquement par setup-certificates.sh
# LiveKit Local
USE_LOCAL_LIVEKIT=true
LIVEKIT_API_KEY=devkey
LIVEKIT_API_SECRET=secret
LIVEKIT_URL=AUTO
# Serveur
PORT=3000
ENABLE_HTTPS=true
# Certificats SSL (chemins absolus)
SSL_CERT=$CERT_FILE
SSL_KEY=$KEY_FILE
# Réseau
NETWORK_IP=$NETWORK_IP
EOF
echo -e "${GREEN}✅ .env serveur mis à jour${NC}"
echo ""
# ========== Configuration Client ==========
echo "⚙️ Configuration client..."
echo ""
CLIENT_ENV="$(pwd)/../client/.env"
cat > "$CLIENT_ENV" << EOF
# PTT Live Client - Configuration
# Généré automatiquement par setup-certificates.sh
VITE_SERVER_URL=https://$NETWORK_IP:3000
EOF
echo -e "${GREEN}✅ .env client créé${NC}"
echo ""
# ========== Mettre à jour Vite Config ==========
echo "⚙️ Configuration Vite HTTPS..."
echo ""
VITE_CONFIG="$(pwd)/../client/vite.config.js"
cat > "$VITE_CONFIG" << EOF
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { VitePWA } from 'vite-plugin-pwa';
import fs from 'fs';
import path from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
VitePWA({
registerType: 'autoUpdate',
includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'masked-icon.svg'],
manifest: {
name: 'PTT Live',
short_name: 'PTT Live',
description: 'Professional WebRTC Intercom',
theme_color: '#1a1a1a',
icons: [
{
src: 'pwa-192x192.png',
sizes: '192x192',
type: 'image/png'
},
{
src: 'pwa-512x512.png',
sizes: '512x512',
type: 'image/png'
}
]
}
})
],
server: {
host: '0.0.0.0',
port: 5173,
https: {
key: fs.readFileSync(path.resolve(__dirname, '../certs/$KEY_FILE')),
cert: fs.readFileSync(path.resolve(__dirname, '../certs/$CERT_FILE'))
}
}
});
EOF
echo -e "${GREEN}✅ vite.config.js mis à jour avec HTTPS${NC}"
echo ""
# ========== Mettre à jour serveur index.js ==========
echo "⚙️ Configuration serveur Express HTTPS..."
echo ""
# Le serveur lira SSL_CERT et SSL_KEY depuis .env
# Pas besoin de modifier index.js si déjà compatible
echo -e "${GREEN}✅ Configuration terminée${NC}"
echo ""
# ========== Récapitulatif ==========
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo -e "${GREEN}✅ CONFIGURATION CERTIFICATS TERMINÉE${NC}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "📜 Certificats générés :"
echo " $CERT_DIR"
echo ""
echo "🌐 URLs d'accès :"
echo ""
echo " Serveur : https://$NETWORK_IP:3000"
echo " Client : https://$NETWORK_IP:5173"
echo ""
echo "🔐 Les certificats sont automatiquement approuvés par :"
echo " - Chrome/Edge/Safari"
echo " - Firefox (si nss installé)"
echo " - Système d'exploitation"
echo ""
echo "📱 Scan QR Code au démarrage pour connexion rapide"
echo ""
echo "🚀 Démarrer le système :"
echo ""
echo " ./start.sh --dev"
echo " # OU"
echo " ./start-desktop.sh"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "💡 Pour smartphones iOS/Android :"
echo ""
echo " 1. Scanner le QR Code affiché au démarrage"
echo " 2. Accepter le certificat (une seule fois)"
echo " 3. Installer la PWA sur l'écran d'accueil"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
# PTT Live - Lancement application desktop
echo "🖥️ Démarrage PTT Live Desktop..."
echo ""
cd electron
npm start