Merge pull request 'feat: migration complète SignalR → Socket.IO (PRD #2)' (#14) from feat/#7-cleanup-signalr into feat/prd-2-socketio-migration

This commit is contained in:
2026-03-19 03:45:09 +01:00
23 changed files with 1410 additions and 5183 deletions

View File

@@ -1,516 +0,0 @@
# Workflow de développement SimpleConnect
Guide complet de A à Z pour le développement et la publication de nouvelles versions.
---
## 📑 Table des matières
1. [Résumé rapide](#résumé-rapide)
2. [Phase 1 : Développement itératif](#phase-1--développement-itératif)
3. [Phase 2 : Préparation de la release](#phase-2--préparation-de-la-release)
4. [Phase 3 : Fusion dans main](#phase-3--fusion-dans-main)
5. [Phase 4 : Build des exécutables](#phase-4--build-des-exécutables)
6. [Phase 5 : Documentation](#phase-5--documentation)
7. [Phase 6 : Publication sur Gitea](#phase-6--publication-sur-gitea)
8. [Phase 7 : Déploiement](#phase-7--déploiement-optionnel)
9. [Phase 8 : Nettoyage](#phase-8--nettoyage-des-branches)
10. [Référence des commandes](#référence-des-commandes)
---
## Résumé rapide
```
Phase 1 : Développement
1. Créer branche feature
2. Développer → Tester → Committer (répéter)
Phase 2 : Release
3. Mettre à jour changelog
4. Bumper version package.json
5. Commit de release
Phase 3 : Fusion
6. Merger dans main (--no-ff)
7. Push vers origin
Phase 4 : Build
8. Build exécutables (macOS, Linux, Windows)
9. Organiser dans dist/vX.X.X/
Phase 5 : Documentation
10. Créer notes de release
11. Commit + push docs
Phase 6 : Publication
12. Créer tag Git
13. Push tag vers Gitea
14. Créer release avec tea
15. Vérifier assets
Phase 7 : Déploiement (optionnel)
16. Déployer sur serveur
Phase 8 : Nettoyage
17. Supprimer branches locales mergées
18. Supprimer branches distantes mergées
19. Prune références locales
```
---
## Phase 1 : Développement itératif
### Principe : Petit à petit, commit par commit
**Une fonctionnalité = plusieurs petits commits atomiques**
Découper le travail en petites étapes testables plutôt qu'un gros commit :
```
Exemple : Ajouter un bouton "Pause" avec confirmation
❌ MAUVAIS : 1 gros commit
git commit -m "feat: Ajouter bouton Pause avec modal et gestion IPBX"
✅ BON : 5 petits commits
1. feat: Ajouter le bouton Pause dans le header
2. style: Ajouter l'icône SVG pour le bouton Pause
3. feat: Implémenter la modal de confirmation
4. feat: Connecter le bouton à SignalR pour pause IPBX
5. docs: Documenter la fonctionnalité de pause
```
### Étape 1 : Créer une branche feature
```bash
git checkout -b feature/nom-de-la-fonctionnalite
```
### Étape 2 : Cycle itératif
Répéter jusqu'à complétion de la fonctionnalité :
#### a) Développer une petite partie
- Faire UNE modification logique à la fois
- Ne pas chercher à tout finir d'un coup
- Rester concentré sur une seule tâche
#### b) Tester
**IMPORTANT : Toujours tester AVANT de committer**
```bash
npm run dev # Test en mode développement
```
- Vérifier que la modification fonctionne
- Tester les cas limites
- S'assurer qu'aucune régression n'a été introduite
#### c) Committer
Seulement après avoir testé avec succès :
```bash
git commit -m "feat: Ajouter le bouton Pause dans le header"
```
**Préfixes de commit** : `feat:`, `fix:`, `refactor:`, `docs:`, `style:`, `test:`, `chore:`
**Pourquoi tester avant de committer ?**
- Historique Git propre (chaque commit = code fonctionnel)
- Rollback facile vers un état stable
- `git bisect` fonctionne correctement
#### d) Continuer
Retour à l'étape **a)** jusqu'à ce que la fonctionnalité soit complète.
### Exception : WIP commits
Pour sauvegarder un travail en cours :
```bash
git commit -m "wip: Travail en cours sur la fonctionnalité X"
# Plus tard, après tests réussis :
git commit --amend -m "feat: Ajouter la fonctionnalité X"
```
---
## Phase 2 : Préparation de la release
### Étape 3 : Mettre à jour le changelog
Fichier : `docs/changelog.md`
```bash
# Obtenir la date
date +%Y-%m-%d
```
**Format** :
```markdown
## [X.X.X] - AAAA-MM-JJ
### Ajouté
- Fonctionnalité 1
### Modifié
- Changement 1
### Corrigé
- Bug 1
```
Catégories : **Ajouté**, **Modifié**, **Corrigé**, **Supprimé**, **Technique**, **Documentation**
### Étape 4 : Bumper la version
Fichier : `package.json`
**Semantic Versioning** :
- **PATCH** (x.x.+1) : Corrections, petits ajustements
- **MINOR** (x.+1.0) : Nouvelles fonctionnalités compatibles
- **MAJOR** (+1.0.0) : Changements majeurs incompatibles
Modifier le champ `"version": "X.X.X"`
### Étape 5 : Commit de release
```bash
git add docs/changelog.md package.json
git commit -m "release: Version X.X.X - Titre court
- Fonctionnalité 1
- Fonctionnalité 2
- Correction 1
- Bump version X.X.X"
```
**Important** : Ne jamais mentionner Claude, Anthropic ou des adresses emails relatives à ces entités.
---
## Phase 3 : Fusion dans main
### Étape 6 : Merger dans main
```bash
git checkout main
git merge feature/nom-de-la-fonctionnalite --no-ff
```
**Option `--no-ff`** : Force un merge commit pour un historique propre et tracé
### Étape 7 : Push vers origin
```bash
git push origin main
```
---
## Phase 4 : Build des exécutables
### Étape 8 : Build pour les plateformes
```bash
# macOS (si développement sur Mac)
npm run build
# Linux x64 (production principale)
npm run build:linux-x64
# Windows (si nécessaire)
npm run build:win
```
### Étape 9 : Organiser les builds
```bash
# Créer le dossier de version
mkdir -p dist/vX.X.X
# Copier le changelog
cp docs/changelog.md dist/vX.X.X/CHANGELOG.md
# Déplacer les builds
mv dist/SimpleConnect-X.X.X*.{AppImage,dmg,zip,exe} dist/vX.X.X/ 2>/dev/null
mv dist/SimpleConnect-X.X.X*.blockmap dist/vX.X.X/ 2>/dev/null
# Vérifier le contenu
ls -lh dist/vX.X.X/
```
---
## Phase 5 : Documentation
### Étape 10 : Créer les notes de release
Fichier : `releases/vX.X.X.md`
**Structure markdown** :
- Titre et date
- Résumé des changements
- Nouveautés principales
- Corrections
- Fichiers disponibles
- Compatibilité
- Guide d'utilisation (si nécessaire)
### Étape 11 : Commit et push de la documentation
```bash
git add releases/vX.X.X.md
git commit -m "docs: Ajout des notes de release pour vX.X.X"
git push origin main
```
---
## Phase 6 : Publication sur Gitea
### Étape 12 : Créer le tag Git
```bash
# Trouver le commit de release
git log --oneline --grep="X.X.X" -5
# Créer le tag annoté
git tag -a vX.X.X <commit-hash> -m "Release vX.X.X - Titre court
Description des principales fonctionnalités
- Fonctionnalité 1
- Fonctionnalité 2
- Correction 1"
```
### Étape 13 : Push du tag
```bash
git push origin vX.X.X
```
### Étape 14 : Créer la release avec `tea`
**Prérequis** : `tea` configuré avec le login `simpleconnect`
```bash
# Vérifier la configuration
tea login list
# Créer la release avec tous les fichiers
tea release create \
--login simpleconnect \
--repo pierre/SimpleConnect-client-electron \
--tag vX.X.X \
--title "SimpleConnect vX.X.X - Titre de la release" \
--note-file releases/vX.X.X.md \
--asset dist/vX.X.X/SimpleConnect-X.X.X.AppImage \
--asset dist/vX.X.X/SimpleConnect-X.X.X-arm64.dmg \
--asset dist/vX.X.X/SimpleConnect-X.X.X-arm64-mac.zip
```
**Notes** :
- `--note-file` utilise le fichier markdown des notes
- Multiple `--asset` pour uploader tous les binaires
- Code source (TAR.GZ et ZIP) ajouté automatiquement par Gitea
### Étape 15 : Vérifier la release
```bash
# Lister toutes les releases
tea release list --login simpleconnect --repo pierre/SimpleConnect-client-electron
# Vérifier les assets d'une release
tea release assets --login simpleconnect --repo pierre/SimpleConnect-client-electron vX.X.X
```
**Résultat attendu** :
- ✅ SimpleConnect-X.X.X.AppImage (Linux x64)
- ✅ SimpleConnect-X.X.X-arm64.dmg (macOS)
- ✅ SimpleConnect-X.X.X-arm64-mac.zip (macOS)
- ✅ Code source (TAR.GZ et ZIP automatiques)
### Corrections éventuelles
**Ajouter des assets manquants** :
```bash
tea release assets create \
--login simpleconnect \
--repo pierre/SimpleConnect-client-electron \
vX.X.X \
dist/vX.X.X/fichier-supplementaire.ext
```
**Supprimer des doublons** :
```bash
tea release assets delete \
--login simpleconnect \
--repo pierre/SimpleConnect-client-electron \
--confirm \
vX.X.X \
nom-du-fichier.ext
```
---
## Phase 7 : Déploiement (optionnel)
### Étape 16 : Déployer sur le serveur
```bash
scp dist/vX.X.X/SimpleConnect-X.X.X.AppImage user@server:/path/to/app/
```
---
## Phase 8 : Nettoyage des branches
Une fois la release publiée et déployée, nettoyer les branches mergées.
### Étape 17 : Supprimer les branches locales
```bash
# Vérifier les branches mergées
git branch --merged main
# Supprimer automatiquement toutes les branches mergées
git branch --merged main | grep -v "^\*" | grep -v "main" | xargs -n 1 git branch -d
# Ou manuellement
git branch -d feature/nom-de-la-fonctionnalite
# Forcer si la branche a un remote (option -D)
git branch -D feature/nom-de-la-fonctionnalite
```
**Pourquoi `-D` peut être nécessaire ?**
- Git refuse `-d` si la branche a encore un remote associé
- Même mergée dans main, Git veut s'assurer qu'elle est mergée dans origin/branche
- `-D` est sûr si on a vérifié le merge dans main
### Étape 18 : Supprimer les branches distantes
```bash
# Supprimer une ou plusieurs branches
git push origin --delete feature/nom-1 feature/nom-2 feature/nom-3
# Vérifier la suppression
git branch -a
```
### Étape 19 : Nettoyer les références locales
```bash
# Supprimer les références aux branches distantes supprimées
git remote prune origin
# Ou avec fetch
git fetch --prune
```
**Résultat attendu** : Seules les branches `main` et celles en cours de travail restent.
---
## Référence des commandes
### Commandes npm
```bash
npm start # Lance l'application en production
npm run dev # Mode développement avec DevTools
npm run build # Build pour toutes les plateformes
npm run build:win # Build Windows
npm run build:mac # Build macOS
npm run build:linux # Build Linux
npm run build:linux-x64 # Build Linux x64
npm run build:linux-arm64 # Build Linux ARM64
```
### Commandes Git
```bash
# Branches
git checkout -b feature/XXX # Créer une branche
git branch --merged main # Lister branches mergées
git branch -d feature/XXX # Supprimer branche locale
git branch -D feature/XXX # Forcer suppression
git push origin --delete XXX # Supprimer branche distante
# Tags
git tag -a vX.X.X <hash> -m "..." # Créer tag annoté
git push origin vX.X.X # Push tag
git tag -l # Lister tags
# Merge
git merge feature/XXX --no-ff # Merge avec commit
# Nettoyage
git remote prune origin # Nettoyer références
git fetch --prune # Fetch + nettoyage
```
### Commandes `tea` (Gitea CLI)
#### Logins
```bash
tea login list # Lister logins configurés
tea login add # Ajouter un login
tea login default <nom> # Définir login par défaut
```
#### Releases
```bash
tea release list --login simpleconnect --repo pierre/SimpleConnect-client-electron
tea release create --help
tea release edit vX.X.X --title "Nouveau titre"
tea release delete vX.X.X --confirm
```
#### Assets
```bash
tea release assets list vX.X.X
tea release assets create vX.X.X fichier.ext
tea release assets delete vX.X.X fichier.ext --confirm
```
#### Options globales
```bash
--login simpleconnect # Utiliser le login 'simpleconnect'
--repo pierre/SimpleConnect-client-electron # Spécifier le repository
--output json # Format de sortie (json, yaml, table)
```
### Conventions de commits
```bash
feat: # Nouvelle fonctionnalité
fix: # Correction de bug
refactor: # Refactoring
docs: # Documentation
style: # Formatage, style
test: # Tests
chore: # Maintenance, tâches diverses
release: # Commit de release
wip: # Work in progress (temporaire)
```
### Semantic Versioning
```
MAJOR.MINOR.PATCH
MAJOR (X.0.0) # Changements incompatibles
MINOR (x.X.0) # Nouvelles fonctionnalités compatibles
PATCH (x.x.X) # Corrections de bugs
```
---
**Dernière mise à jour** : 2025-10-21

View File

@@ -3,17 +3,18 @@
Application desktop de télésecrétariat pour les postes RDVPREM. Application desktop de télésecrétariat pour les postes RDVPREM.
## Stack ## Stack
- **Electron 28** + **SignalR** (@microsoft/signalr 9.0.6) + **Choices.js** 11.1.0 - **Electron 28** + **Socket.IO** (socket.io-client 4.8.1) + **Choices.js** 11.1.0
- HTML/CSS/JavaScript natif (pas de framework frontend) - HTML/CSS/JavaScript natif (pas de framework frontend)
## Structure ## Structure
``` ```
├── main.js # Process principal — SignalR, IPC, fenêtres ├── main.js # Process principal — Socket.IO, IPC, fenêtres
├── socketio-adapter.js # Adaptateur Socket.IO (connect/logoff/disconnect)
├── renderer.js # Process renderer — UI, webviews ├── renderer.js # Process renderer — UI, webviews
├── index.html # Structure HTML ├── index.html # Structure HTML
├── styles-modern.css # Styles CSS ├── styles-modern.css # Styles CSS
├── config.json # Config SignalR (host, port, serviceProvider) ├── config.json # Config Socket.IO (serverUrl, serviceProvider)
└── notes/ # Stockage notes agents (notes_{agentId}.json) └── notes/ # Stockage notes agents (notes_{agentId}.json)
``` ```
@@ -27,15 +28,24 @@ bun run build:linux # Build Linux (AppImage, .deb, .rpm)
bun run build:mac # Build macOS (.dmg, .app) bun run build:mac # Build macOS (.dmg, .app)
``` ```
## Tests
```bash
bun test # 8 tests unitaires socketio-adapter
```
- `socketio-adapter.js` accepte un socket factory en 2e param (injection pour tests)
- Fake socket avec EventEmitter minimal dans `socketio-adapter.test.js`
## Points d'attention ## Points d'attention
- **Pas d'emojis** dans l'UI — icônes SVG inline (compatibilité Linux) - **Pas d'emojis** dans l'UI — icônes SVG inline (compatibilité Linux)
- **Sessions webview isolées** : partition Electron unique par centre, auto-connexion via preload script - **Sessions webview isolées** : partition Electron unique par centre, auto-connexion via preload script
- **SignalR reconnexion** : [0, 2000, 5000, 10000]ms - **Socket.IO reconnexion** : illimitée (2s→10s backoff)
- **IPC principal** : `agent-login`, `get-terminals`, `signalr-status`, `switch-to-center`, `release-center` - **IPC principal** : `login-agent`, `get-terminal-list`, `server-status`, `switch-to-center`, `release-center`
- **Hub SignalR** : `/planningHub` — méthodes `AgentLogin`, `AgentLogoff`, `GetTerminalListByServiceProvider` - **Protocole serveur** : auth au handshake, events `login_ok`/`login_error`/`ipbx_event`/`logout``logout_ok`
- **Événement `IpbxEvent`** : codes 1=appel entrant, 2=fin d'appel - **Terminaux** : REST `GET /terminals?provider=RDVPREM` (pas Socket.IO)
- **Logs SignalR** : `~/.simpleconnect-ng/signalr.log` (JSON structuré) - **Logs** : `~/.simpleconnect-ng/socketio.log`
- **Notes** : sauvegarde auto après 2s d'inactivité, 50 versions, sync localStorage + fichier - **Notes** : sauvegarde auto après 2s d'inactivité, 50 versions, sync localStorage + fichier
## Workflow de développement ## Workflow de développement

251
README.md
View File

@@ -1,251 +0,0 @@
# SimpleConnect - Application de Gestion Centralisée des Plannings Médicaux
Application Electron destinée aux centres d'appels médicaux pour la gestion unifiée de multiples plateformes de prise de rendez-vous.
## 🎯 Fonctionnalités principales
### Gestion centralisée
- **Connexion unique** : Authentification centralisée avec sélection du terminal téléphonique
- **Multi-centres** : Accès simultané à plusieurs plateformes (Doctolib, MonDocteur, etc.)
- **Navigation par onglets** : Bascule rapide entre les différents centres assignés
- **Sessions isolées** : Connexions automatiques et indépendantes pour chaque centre
### Intégration CTI (Computer Telephony Integration)
- **Connexion SignalR/WebSocket** : Communication temps réel avec le serveur IPBX avec fallback automatique
- **Gestion des appels entrants** : Notifications et bascule automatique vers le bon centre
- **Terminaux téléphoniques** : Sélection dynamique ou saisie manuelle du poste lors de la connexion
- **Événements IPBX** : Support complet des événements téléphoniques (sonnerie, décrochage, raccrochage)
### Outils de productivité
- **Panneau de notes latéral** :
- Redimensionnable (280px à 600px de largeur)
- Sauvegarde automatique après 2 secondes d'inactivité
- Historique des 50 dernières versions
- Synchronisation localStorage + fichier serveur
- **Statistiques en temps réel** : Compteur d'appels et de rendez-vous
- **Mode développement** : Simulation d'appels pour les tests
## 🚀 Installation
### Prérequis
- Node.js version 16 ou supérieure
- npm ou yarn
- Windows, macOS ou Linux
### Installation rapide
```bash
# Cloner le repository
git clone https://github.com/simpleconnect/electron-app.git
cd simpleconnect-electron
# Installer les dépendances
npm install
# Lancer l'application
npm start
```
### Scripts disponibles
```bash
npm start # Lancer l'application en production
npm run dev # Mode développement avec DevTools
npm run build # Builder pour toutes les plateformes
npm run build:win # Builder pour Windows
npm run build:mac # Builder pour macOS
npm run build:linux # Builder pour Linux
```
## ⚙️ Configuration
### Fichier config.json
```json
{
"signalR": {
"enabled": true,
"serverUrl": "http://votre-serveur-signalr:8080/planningHub",
"reconnectInterval": 5000
}
}
```
### Variables d'environnement
- `NODE_ENV=development` : Active le mode développement avec DevTools
## 📱 Utilisation
### 1. Connexion agent
1. **Lancer l'application** SimpleConnect
2. **Vérifier l'indicateur SignalR** (vert = connecté)
3. **Saisir les identifiants** :
- Code agent
- Mot de passe
- Sélection ou saisie manuelle du terminal téléphonique
4. **Option "Débloquer"** si session bloquée
5. **Bouton "Quitter"** pour fermer l'application sans se connecter
### 2. Interface principale
- **Header** : Nom de l'agent et statut des appels
- **Onglets** : Un par centre assigné (badge pour appels actifs)
- **Zone centrale** : Webview du planning sélectionné
- **Panneau de notes** : Accessible via le bouton 📝
- **Bouton Quitter** : Déconnexion et fermeture de l'application
### 3. Gestion des appels
1. **Appel entrant** :
- Notification avec informations patient
- Son de sonnerie
- Bouton "Prendre l'appel"
2. **Prise d'appel** :
- Bascule automatique vers le bon centre
- Badge rouge sur l'onglet actif
- Mise à jour du statut d'appel
3. **Pendant l'appel** :
- Prise de rendez-vous dans la webview
- Notes dans le panneau latéral
- Sauvegarde automatique
4. **Fin d'appel** :
- Détection automatique du raccrochage
- Incrémentation des statistiques
- Notes conservées
## 🏗️ Architecture technique
### Stack technologique
- **Frontend** : HTML5, CSS3, JavaScript ES6+
- **Framework** : Electron 28.0.0
- **Communication temps réel** :
- SignalR (@microsoft/signalr 9.0.6)
- WebSocket avec fallback automatique (socket.io-client 4.8.1)
- ConnectionManager pour basculement transparent
- **UI Components** : Choices.js 11.1.0 pour les selects personnalisés
- **Stockage** : Fichiers JSON locaux + localStorage
### Structure du projet
```
simpleconnect-electron/
├── main.js # Process principal Electron et gestion SignalR/WebSocket
├── renderer.js # Interface utilisateur et gestion des webviews
├── connection-manager.js # Gestionnaire de connexion avec fallback automatique
├── websocket-adapter.js # Adaptateur SocketIO émulant l'API SignalR
├── index.html # Structure HTML de l'application
├── styles-modern.css # Styles CSS modernes avec gradients
├── config.json # Configuration SignalR et paramètres
├── docs/
│ └── changelog.md # Historique complet des versions
├── notes/ # Dossier de stockage des notes agents
└── package.json # Dépendances et métadonnées
```
### Communication IPC
Principaux canaux :
- `login-agent` : Authentification via SignalR/WebSocket
- `logout` / `quit-app` : Déconnexion et fermeture
- `get-terminal-list` : Liste des terminaux disponibles
- `save-notes` / `get-notes` : Gestion des notes
- `signalr-status` : État de la connexion temps réel
- `get-app-version` : Récupération de la version de l'application
- `switch-to-center` / `release-center` : Événements IPBX pour la gestion des appels
## 🔒 Sécurité
- **Isolation des sessions** : Partition Electron par centre
- **Connexion sécurisée** : Support HTTPS/WSS pour SignalR et WebSocket
- **Pas de stockage de mots de passe** : Authentification directe serveur
- **Logging détaillé** : Tous les événements SignalR dans `~/.simpleconnect-ng/signalr.log`
## 📊 Versions
### Version actuelle : 1.4.1
Voir [changelog.md](docs/changelog.md) pour l'historique complet des versions.
### Dernières nouveautés
#### v1.4.1 (2025-10-21)
- Bouton "Quitter" sur la page de connexion pour fermer l'application sans se connecter
- Style secondaire pour différenciation visuelle du bouton principal
#### v1.4.0 (2025-10-21)
- Affichage de la version dans l'interface (page de login et header principal)
- Version affichée dans la barre de titre native : "SimpleConnect vX.X.X"
#### v1.3.1 (2025-10-17)
- Saisie manuelle de postes téléphoniques personnalisés
- Validation de format numérique avec avertissement non-bloquant
- Affichage simplifié des terminaux (sans préfixe "Poste")
- Amélioration du contraste de l'élément survolé dans la liste déroulante
#### v1.3.0 (2025-09-12)
- Support dual SignalR/SocketIO avec fallback automatique
- Compatibilité totale avec backends .NET et Python
- ConnectionManager qui essaie SignalR puis bascule sur SocketIO
- WebSocketAdapter émulant l'API SignalR complète
#### v1.2.x
- Panneau de notes redimensionnable avec sauvegarde automatique
- Système de persistance amélioré avec historique (50 versions)
- Interface moderne avec animations fluides
- Logging complet des événements SignalR
## 🐛 Dépannage
### Problèmes courants
1. **SignalR/WebSocket ne se connecte pas** :
- Vérifier l'URL du serveur dans config.json
- Vérifier la connexion réseau
- Consulter les logs dans `~/.simpleconnect-ng/signalr.log`
- L'application bascule automatiquement sur WebSocket si SignalR échoue
- Mode développement (`npm run dev`) pour voir les erreurs console
2. **Session bloquée** :
- Utiliser l'option "Débloquer" sur la page de connexion
- Cette option force la déconnexion de la session précédente
- Redémarrer l'application si nécessaire
3. **Poste téléphonique personnalisé non reconnu** :
- Le système affiche un avertissement mais accepte tout numéro valide
- Vérifier que le poste est bien numérique (ex: 3001)
- Le serveur valide la disponibilité du terminal
4. **Webviews ne se chargent pas** :
- Vérifier la connexion internet
- Vérifier les URLs des centres dans la configuration serveur
- Utiliser le bouton Rafraîchir dans le header pour recharger la webview
- Mode développement pour voir les erreurs console
5. **Notes non sauvegardées** :
- Vérifier que le dossier `notes/` existe et est accessible
- La sauvegarde automatique se déclenche après 2 secondes d'inactivité
- Les notes sont aussi sauvegardées dans localStorage comme backup
## 🚧 Roadmap
- [ ] Support multi-langues
- [ ] Mode sombre/clair
- [ ] Export des statistiques en CSV/PDF
- [ ] Intégration avec plus de plateformes de RDV
- [ ] Application mobile companion
- [ ] Dashboard manager avec métriques temps réel
## 📝 Licence
MIT - Voir le fichier [LICENSE](LICENSE) pour plus de détails.
## 🤝 Support
Pour toute question, problème ou suggestion :
- Créer une issue sur [GitHub](https://github.com/simpleconnect/electron-app/issues)
- Documentation complète dans le dossier [docs/](docs/)
---
**SimpleConnect** - Simplifier la gestion des rendez-vous médicaux pour les centres d'appels

660
bun.lock Normal file
View File

@@ -0,0 +1,660 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "simpleconnect-electron",
"dependencies": {
"choices.js": "^11.1.0",
"socket.io-client": "^4.8.1",
},
"devDependencies": {
"electron": "^28.0.0",
"electron-builder": "^24.9.1",
},
},
},
"packages": {
"7zip-bin": ["7zip-bin@5.2.0", "", {}, "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A=="],
"@develar/schema-utils": ["@develar/schema-utils@2.6.5", "", { "dependencies": { "ajv": "^6.12.0", "ajv-keywords": "^3.4.1" } }, "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig=="],
"@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
"@electron/get": ["@electron/get@2.0.3", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ=="],
"@electron/notarize": ["@electron/notarize@2.2.1", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.1", "promise-retry": "^2.0.1" } }, "sha512-aL+bFMIkpR0cmmj5Zgy0LMKEpgy43/hw5zadEArgmAMWWlKc5buwFvFT9G/o/YJkvXAJm5q3iuTuLaiaXW39sg=="],
"@electron/osx-sign": ["@electron/osx-sign@1.0.5", "", { "dependencies": { "compare-version": "^0.1.2", "debug": "^4.3.4", "fs-extra": "^10.0.0", "isbinaryfile": "^4.0.8", "minimist": "^1.2.6", "plist": "^3.0.5" }, "bin": { "electron-osx-flat": "bin/electron-osx-flat.js", "electron-osx-sign": "bin/electron-osx-sign.js" } }, "sha512-k9ZzUQtamSoweGQDV2jILiRIHUu7lYlJ3c6IEmjv1hC17rclE+eb9U+f6UFlOOETo0JzY1HNlXy4YOlCvl+Lww=="],
"@electron/universal": ["@electron/universal@1.5.1", "", { "dependencies": { "@electron/asar": "^3.2.1", "@malept/cross-spawn-promise": "^1.1.0", "debug": "^4.3.1", "dir-compare": "^3.0.0", "fs-extra": "^9.0.1", "minimatch": "^3.0.4", "plist": "^3.0.4" } }, "sha512-kbgXxyEauPJiQQUNG2VgUeyfQNFk6hBF11ISN2PNI6agUgPl55pv4eQmaqHzTAzchBvqZ2tQuRVaPStGf0mxGw=="],
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
"@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@1.1.1", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ=="],
"@malept/flatpak-bundler": ["@malept/flatpak-bundler@0.4.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.0", "lodash": "^4.17.15", "tmp-promise": "^3.0.2" } }, "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q=="],
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
"@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="],
"@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="],
"@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="],
"@tootallnate/once": ["@tootallnate/once@2.0.0", "", {}, "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="],
"@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="],
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
"@types/fs-extra": ["@types/fs-extra@9.0.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA=="],
"@types/http-cache-semantics": ["@types/http-cache-semantics@4.2.0", "", {}, "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q=="],
"@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="],
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
"@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
"@types/plist": ["@types/plist@3.0.5", "", { "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" } }, "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA=="],
"@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="],
"@types/verror": ["@types/verror@1.10.11", "", {}, "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg=="],
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="],
"agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
"ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
"ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"app-builder-bin": ["app-builder-bin@4.0.0", "", {}, "sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA=="],
"app-builder-lib": ["app-builder-lib@24.13.3", "", { "dependencies": { "@develar/schema-utils": "~2.6.5", "@electron/notarize": "2.2.1", "@electron/osx-sign": "1.0.5", "@electron/universal": "1.5.1", "@malept/flatpak-bundler": "^0.4.0", "@types/fs-extra": "9.0.13", "async-exit-hook": "^2.0.1", "bluebird-lst": "^1.0.9", "builder-util": "24.13.1", "builder-util-runtime": "9.2.4", "chromium-pickle-js": "^0.2.0", "debug": "^4.3.4", "ejs": "^3.1.8", "electron-publish": "24.13.1", "form-data": "^4.0.0", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "is-ci": "^3.0.0", "isbinaryfile": "^5.0.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", "minimatch": "^5.1.1", "read-config-file": "6.3.2", "sanitize-filename": "^1.6.3", "semver": "^7.3.8", "tar": "^6.1.12", "temp-file": "^3.4.0" }, "peerDependencies": { "dmg-builder": "24.13.3", "electron-builder-squirrel-windows": "24.13.3" } }, "sha512-FAzX6IBit2POXYGnTCT8YHFO/lr5AapAII6zzhQO3Rw4cEDOgK+t1xhLc5tNcKlicTHlo9zxIwnYCX9X2DLkig=="],
"archiver": ["archiver@5.3.2", "", { "dependencies": { "archiver-utils": "^2.1.0", "async": "^3.2.4", "buffer-crc32": "^0.2.1", "readable-stream": "^3.6.0", "readdir-glob": "^1.1.2", "tar-stream": "^2.2.0", "zip-stream": "^4.1.0" } }, "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw=="],
"archiver-utils": ["archiver-utils@2.1.0", "", { "dependencies": { "glob": "^7.1.4", "graceful-fs": "^4.2.0", "lazystream": "^1.0.0", "lodash.defaults": "^4.2.0", "lodash.difference": "^4.5.0", "lodash.flatten": "^4.4.0", "lodash.isplainobject": "^4.0.6", "lodash.union": "^4.6.0", "normalize-path": "^3.0.0", "readable-stream": "^2.0.0" } }, "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"assert-plus": ["assert-plus@1.0.0", "", {}, "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="],
"astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="],
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
"async-exit-hook": ["async-exit-hook@2.0.1", "", {}, "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw=="],
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
"at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
"bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="],
"bluebird-lst": ["bluebird-lst@1.0.9", "", { "dependencies": { "bluebird": "^3.5.5" } }, "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw=="],
"boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="],
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
"buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
"buffer-equal": ["buffer-equal@1.0.1", "", {}, "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg=="],
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"builder-util": ["builder-util@24.13.1", "", { "dependencies": { "7zip-bin": "~5.2.0", "@types/debug": "^4.1.6", "app-builder-bin": "4.0.0", "bluebird-lst": "^1.0.9", "builder-util-runtime": "9.2.4", "chalk": "^4.1.2", "cross-spawn": "^7.0.3", "debug": "^4.3.4", "fs-extra": "^10.1.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.1", "is-ci": "^3.0.0", "js-yaml": "^4.1.0", "source-map-support": "^0.5.19", "stat-mode": "^1.0.0", "temp-file": "^3.4.0" } }, "sha512-NhbCSIntruNDTOVI9fdXz0dihaqX2YuE1D6zZMrwiErzH4ELZHE6mdiB40wEgZNprDia+FghRFgKoAqMZRRjSA=="],
"builder-util-runtime": ["builder-util-runtime@9.2.4", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA=="],
"cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="],
"cacheable-request": ["cacheable-request@7.0.4", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^6.0.1", "responselike": "^2.0.0" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"choices.js": ["choices.js@11.2.1", "", { "dependencies": { "fuse.js": "^7.0.0" } }, "sha512-SyA0FRr0+UHOjKO/9oR0/hVYRgk3v5C9ZD+CLnQfLFR7ZSiNb/rQeAUOsqXaPTk/552X5dtvRaDiG94Ga7G2Gg=="],
"chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
"chromium-pickle-js": ["chromium-pickle-js@0.2.0", "", {}, "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw=="],
"ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="],
"cli-truncate": ["cli-truncate@2.1.0", "", { "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" } }, "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg=="],
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
"commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="],
"compare-version": ["compare-version@0.1.2", "", {}, "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A=="],
"compress-commons": ["compress-commons@4.1.2", "", { "dependencies": { "buffer-crc32": "^0.2.13", "crc32-stream": "^4.0.2", "normalize-path": "^3.0.0", "readable-stream": "^3.6.0" } }, "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"config-file-ts": ["config-file-ts@0.2.6", "", { "dependencies": { "glob": "^10.3.10", "typescript": "^5.3.3" } }, "sha512-6boGVaglwblBgJqGyxm4+xCmEGcWgnWHSWHY5jad58awQhB6gftq0G8HbzU39YqCIYHMLAiL1yjwiZ36m/CL8w=="],
"core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="],
"crc": ["crc@3.8.0", "", { "dependencies": { "buffer": "^5.1.0" } }, "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ=="],
"crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="],
"crc32-stream": ["crc32-stream@4.0.3", "", { "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^3.4.0" } }, "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
"defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="],
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
"detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="],
"dir-compare": ["dir-compare@3.3.0", "", { "dependencies": { "buffer-equal": "^1.0.0", "minimatch": "^3.0.4" } }, "sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg=="],
"dmg-builder": ["dmg-builder@24.13.3", "", { "dependencies": { "app-builder-lib": "24.13.3", "builder-util": "24.13.1", "builder-util-runtime": "9.2.4", "fs-extra": "^10.1.0", "iconv-lite": "^0.6.2", "js-yaml": "^4.1.0" }, "optionalDependencies": { "dmg-license": "^1.0.11" } }, "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ=="],
"dmg-license": ["dmg-license@1.0.11", "", { "dependencies": { "@types/plist": "^3.0.1", "@types/verror": "^1.10.3", "ajv": "^6.10.0", "crc": "^3.8.0", "iconv-corefoundation": "^1.1.7", "plist": "^3.0.4", "smart-buffer": "^4.0.2", "verror": "^1.10.0" }, "os": "darwin", "bin": { "dmg-license": "bin/dmg-license.js" } }, "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q=="],
"dotenv": ["dotenv@9.0.2", "", {}, "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg=="],
"dotenv-expand": ["dotenv-expand@5.1.0", "", {}, "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
"electron": ["electron@28.3.3", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^18.11.18", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-ObKMLSPNhomtCOBAxFS8P2DW/4umkh72ouZUlUKzXGtYuPzgr1SYhskhFWgzAsPtUzhL2CzyV2sfbHcEW4CXqw=="],
"electron-builder": ["electron-builder@24.13.3", "", { "dependencies": { "app-builder-lib": "24.13.3", "builder-util": "24.13.1", "builder-util-runtime": "9.2.4", "chalk": "^4.1.2", "dmg-builder": "24.13.3", "fs-extra": "^10.1.0", "is-ci": "^3.0.0", "lazy-val": "^1.0.5", "read-config-file": "6.3.2", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "cli.js", "install-app-deps": "install-app-deps.js" } }, "sha512-yZSgVHft5dNVlo31qmJAe4BVKQfFdwpRw7sFp1iQglDRCDD6r22zfRJuZlhtB5gp9FHUxCMEoWGq10SkCnMAIg=="],
"electron-builder-squirrel-windows": ["electron-builder-squirrel-windows@24.13.3", "", { "dependencies": { "app-builder-lib": "24.13.3", "archiver": "^5.3.1", "builder-util": "24.13.1", "fs-extra": "^10.1.0" } }, "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg=="],
"electron-publish": ["electron-publish@24.13.1", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "builder-util": "24.13.1", "builder-util-runtime": "9.2.4", "chalk": "^4.1.2", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-2ZgdEqJ8e9D17Hwp5LEq5mLQPjqU3lv/IALvgp+4W8VeNhryfGhYEQC/PgDPMrnWUp+l60Ou5SJLsu+k4mhQ8A=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
"engine.io-client": ["engine.io-client@6.6.4", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.18.3", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw=="],
"engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="],
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
"err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
"es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="],
"extsprintf": ["extsprintf@1.4.1", "", {}, "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
"filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="],
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
"fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
"fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
"fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="],
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"fuse.js": ["fuse.js@7.1.0", "", {}, "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
"glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
"global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="],
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"got": ["got@11.8.6", "", { "dependencies": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", "@types/cacheable-request": "^6.0.1", "@types/responselike": "^1.0.0", "cacheable-lookup": "^5.0.3", "cacheable-request": "^7.0.2", "decompress-response": "^6.0.0", "http2-wrapper": "^1.0.0-beta.5.2", "lowercase-keys": "^2.0.0", "p-cancelable": "^2.0.0", "responselike": "^2.0.0" } }, "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="],
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
"http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="],
"http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="],
"https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
"iconv-corefoundation": ["iconv-corefoundation@1.1.7", "", { "dependencies": { "cli-truncate": "^2.1.0", "node-addon-api": "^1.6.3" }, "os": "darwin" }, "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ=="],
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"is-ci": ["is-ci@3.0.1", "", { "dependencies": { "ci-info": "^3.2.0" }, "bin": { "is-ci": "bin.js" } }, "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
"isbinaryfile": ["isbinaryfile@5.0.7", "", {}, "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" } }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="],
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"lazy-val": ["lazy-val@1.0.5", "", {}, "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q=="],
"lazystream": ["lazystream@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="],
"lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="],
"lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
"lodash.difference": ["lodash.difference@4.5.0", "", {}, "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA=="],
"lodash.flatten": ["lodash.flatten@4.4.0", "", {}, "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g=="],
"lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="],
"lodash.union": ["lodash.union@4.6.0", "", {}, "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw=="],
"lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="],
"lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
"matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="],
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
"minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="],
"minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="],
"mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"node-addon-api": ["node-addon-api@1.7.2", "", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="],
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
"normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="],
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="],
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
"pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="],
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
"progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
"promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="],
"pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="],
"read-config-file": ["read-config-file@6.3.2", "", { "dependencies": { "config-file-ts": "^0.2.4", "dotenv": "^9.0.2", "dotenv-expand": "^5.1.0", "js-yaml": "^4.1.0", "json5": "^2.2.0", "lazy-val": "^1.0.4" } }, "sha512-M80lpCjnE6Wt6zb98DoW8WHR09nzMSpu8XHtPkiTHrJ5Az9CybfeQhTJ8D7saeBHpGhLPIVyA8lcL6ZmdKwY6Q=="],
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"readdir-glob": ["readdir-glob@1.1.3", "", { "dependencies": { "minimatch": "^5.1.0" } }, "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA=="],
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="],
"responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="],
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
"roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
"safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"sanitize-filename": ["sanitize-filename@1.6.3", "", { "dependencies": { "truncate-utf8-bytes": "^1.0.0" } }, "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg=="],
"sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="],
"serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="],
"slice-ansi": ["slice-ansi@3.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ=="],
"smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
"socket.io-client": ["socket.io-client@4.8.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g=="],
"socket.io-parser": ["socket.io-parser@4.2.6", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg=="],
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
"sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
"stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="],
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="],
"tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
"temp-file": ["temp-file@3.4.0", "", { "dependencies": { "async-exit-hook": "^2.0.1", "fs-extra": "^10.0.0" } }, "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg=="],
"tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="],
"tmp-promise": ["tmp-promise@3.0.3", "", { "dependencies": { "tmp": "^0.2.0" } }, "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ=="],
"truncate-utf8-bytes": ["truncate-utf8-bytes@1.0.2", "", { "dependencies": { "utf8-byte-length": "^1.0.1" } }, "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ=="],
"type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
"xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="],
"xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
"zip-stream": ["zip-stream@4.1.1", "", { "dependencies": { "archiver-utils": "^3.0.4", "compress-commons": "^4.1.2", "readable-stream": "^3.6.0" } }, "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ=="],
"@electron/asar/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"@electron/asar/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
"@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
"@electron/notarize/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
"@electron/osx-sign/isbinaryfile": ["isbinaryfile@4.0.10", "", {}, "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw=="],
"@electron/universal/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
"@electron/universal/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"@isaacs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
"@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
"@types/cacheable-request/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
"@types/fs-extra/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
"@types/keyv/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
"@types/plist/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
"@types/responselike/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
"@types/yauzl/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
"app-builder-lib/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"archiver-utils/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"archiver-utils/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="],
"dir-compare/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
"fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
"glob/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
"global-agent/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"path-scurry/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
"simple-update-notifier/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"zip-stream/archiver-utils": ["archiver-utils@3.0.4", "", { "dependencies": { "glob": "^7.2.3", "graceful-fs": "^4.2.0", "lazystream": "^1.0.0", "lodash.defaults": "^4.2.0", "lodash.difference": "^4.5.0", "lodash.flatten": "^4.4.0", "lodash.isplainobject": "^4.0.6", "lodash.union": "^4.6.0", "normalize-path": "^3.0.0", "readable-stream": "^3.6.0" } }, "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw=="],
"@electron/asar/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
"@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
"@electron/universal/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"@types/cacheable-request/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"@types/fs-extra/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"@types/keyv/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"@types/plist/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"@types/responselike/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"@types/yauzl/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"archiver-utils/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
"archiver-utils/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"dir-compare/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"zip-stream/archiver-utils/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"archiver-utils/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"zip-stream/archiver-utils/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
"zip-stream/archiver-utils/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
}
}

View File

@@ -1,8 +1,6 @@
{ {
"signalR": { "socketio": {
"enabled": true, "serverUrl": "http://10.90.20.201:8004",
"serverUrl": "http://10.90.20.201:8002/signalR", "serviceProvider": "RDVPREM"
"serviceProvider": "RDVPREM",
"terminalsSimulation": ["3001", "3002", "3003", "3004", "3005"]
} }
} }

View File

@@ -1,133 +0,0 @@
/**
* Gestionnaire de connexion avec fallback SignalR → REST + Socket.IO
*
* 1. Essaie d'abord SignalR (serveur .NET legacy)
* 2. Si SignalR échoue, bascule sur REST + Socket.IO (serveur Python)
*/
const signalR = require('@microsoft/signalr');
const RestSocketAdapter = require('./rest-socket-adapter');
class ConnectionManager {
constructor(serverUrl, options = {}) {
this.serverUrl = serverUrl;
this.options = options;
this.connection = null;
this.connectionType = 'none'; // 'none', 'SignalR', 'REST+SocketIO'
}
/**
* Établit la connexion en essayant SignalR puis REST + Socket.IO
*/
async connect() {
console.log('🔌 Tentative de connexion au serveur...');
// 1. Essayer SignalR d'abord (serveur .NET)
try {
console.log('📡 Essai avec SignalR...');
this.connection = this._createSignalRConnection();
await this.connection.start();
this.connectionType = 'SignalR';
console.log('✅ Connexion SignalR établie (serveur .NET)');
return this.connection;
} catch (signalRError) {
console.warn('⚠️ SignalR indisponible:', signalRError.message);
console.log('🔄 Basculement vers REST + Socket.IO...');
}
// 2. Fallback vers REST + Socket.IO (serveur Python)
try {
this.connection = new RestSocketAdapter(this.serverUrl, this.options);
await this.connection.start();
this.connectionType = 'REST+SocketIO';
console.log('✅ Connexion REST + Socket.IO établie (serveur Python)');
return this.connection;
} catch (restError) {
console.error('❌ REST + Socket.IO indisponible:', restError.message);
throw new Error('Impossible de se connecter (SignalR et REST+SocketIO ont échoué)');
}
}
/**
* Crée une connexion SignalR
*/
_createSignalRConnection() {
let url = this.serverUrl;
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = `http://${url}`;
}
if (!url.includes('/signalR')) {
url = `${url}/signalR`;
}
console.log(`📡 URL SignalR: ${url}`);
return new signalR.HubConnectionBuilder()
.withUrl(url)
.withAutomaticReconnect([0, 2000, 5000, 10000])
.configureLogging(signalR.LogLevel.Information)
.build();
}
/**
* Retourne la connexion active
*/
getConnection() {
return this.connection;
}
/**
* Déconnecte proprement
*/
async disconnect() {
if (this.connection) {
await this.connection.stop();
this.connection = null;
this.connectionType = 'none';
}
}
/**
* Informations sur la connexion
*/
getConnectionInfo() {
return {
type: this.connectionType,
isSignalR: this.connectionType === 'SignalR',
isRestSocketIO: this.connectionType === 'REST+SocketIO',
isConnected: this.connection && this.connection.state === 'Connected',
serverUrl: this.serverUrl
};
}
/**
* Méthode helper pour invoquer des méthodes sur la connexion
*/
async invoke(methodName, ...args) {
if (!this.connection) {
throw new Error('Pas de connexion active');
}
return this.connection.invoke(methodName, ...args);
}
/**
* Méthode helper pour écouter des événements
*/
on(eventName, handler) {
if (!this.connection) {
throw new Error('Pas de connexion active');
}
this.connection.on(eventName, handler);
}
/**
* Méthode helper pour retirer un handler d'événement
*/
off(eventName) {
if (this.connection && this.connection.off) {
this.connection.off(eventName);
}
}
}
module.exports = ConnectionManager;

View File

@@ -1,313 +0,0 @@
// Module de simulation CTI (Computer Telephony Integration)
// Ce module simule l'intégration téléphonique pour les tests
class CTISimulator {
constructor() {
this.isRunning = false;
this.autoCallInterval = null;
this.callHistory = [];
}
// Démarrer la simulation automatique d'appels
startAutoSimulation(intervalSeconds = 60) {
if (this.isRunning) return;
this.isRunning = true;
console.log(`Simulation CTI démarrée - Appels toutes les ${intervalSeconds} secondes`);
// Premier appel après 10 secondes
setTimeout(() => {
this.generateRandomCall();
}, 10000);
// Puis appels réguliers
this.autoCallInterval = setInterval(() => {
if (Math.random() > 0.3) { // 70% de chance d'avoir un appel
this.generateRandomCall();
}
}, intervalSeconds * 1000);
}
// Arrêter la simulation
stopAutoSimulation() {
if (this.autoCallInterval) {
clearInterval(this.autoCallInterval);
this.autoCallInterval = null;
}
this.isRunning = false;
console.log('Simulation CTI arrêtée');
}
// Générer un appel aléatoire
async generateRandomCall() {
const config = await ipcRenderer.invoke('get-config');
const simulatedCalls = config.cti.appelSimules;
if (!simulatedCalls || simulatedCalls.length === 0) return;
// Sélectionner un appel au hasard
const randomCall = simulatedCalls[Math.floor(Math.random() * simulatedCalls.length)];
// Vérifier que l'agent a accès à ce centre
const currentAgent = await ipcRenderer.invoke('get-current-agent');
if (!currentAgent || !currentAgent.agent.centresAssignes.includes(randomCall.centreId)) {
// Essayer avec un autre appel
const validCalls = simulatedCalls.filter(call =>
currentAgent.agent.centresAssignes.includes(call.centreId)
);
if (validCalls.length > 0) {
const validCall = validCalls[Math.floor(Math.random() * validCalls.length)];
this.triggerIncomingCall(validCall);
}
} else {
this.triggerIncomingCall(randomCall);
}
}
// Déclencher un appel entrant
triggerIncomingCall(callData) {
console.log('Appel entrant simulé:', callData);
// Ajouter des métadonnées
const enrichedCall = {
...callData,
id: this.generateCallId(),
timestamp: new Date().toISOString(),
duration: 0,
status: 'ringing'
};
// Ajouter à l'historique
this.callHistory.push(enrichedCall);
// Déclencher l'événement
ipcRenderer.invoke('simulate-call', enrichedCall);
}
// Générer un ID unique pour l'appel
generateCallId() {
return `CALL-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
// Obtenir les statistiques des appels
getCallStats() {
const now = new Date();
const todayStart = new Date(now.setHours(0, 0, 0, 0));
const todayCalls = this.callHistory.filter(call =>
new Date(call.timestamp) >= todayStart
);
return {
total: this.callHistory.length,
today: todayCalls.length,
answered: todayCalls.filter(c => c.status === 'answered').length,
missed: todayCalls.filter(c => c.status === 'missed').length,
average_duration: this.calculateAverageDuration(todayCalls)
};
}
// Calculer la durée moyenne des appels
calculateAverageDuration(calls) {
const answered = calls.filter(c => c.duration > 0);
if (answered.length === 0) return 0;
const total = answered.reduce((sum, call) => sum + call.duration, 0);
return Math.round(total / answered.length);
}
// Simuler différents scénarios d'appels
simulateScenarios() {
const scenarios = [
{
name: 'Appel urgent',
setup: () => {
this.triggerIncomingCall({
numero: '+33699887766',
nom: 'URGENT - Marie LAMBERT',
centreId: 'centre1',
priority: 'high',
motif: 'Consultation urgente cardiologie'
});
}
},
{
name: 'Patient régulier',
setup: () => {
this.triggerIncomingCall({
numero: '+33612345678',
nom: 'Jean DUPUIS (Patient régulier)',
centreId: 'centre2',
priority: 'normal',
motif: 'Suivi mensuel',
lastVisit: '2024-11-15'
});
}
},
{
name: 'Nouveau patient',
setup: () => {
this.triggerIncomingCall({
numero: '+33755443322',
nom: 'Sophie MARTIN',
centreId: 'centre3',
priority: 'normal',
motif: 'Première consultation',
isNew: true
});
}
},
{
name: 'Appel pour résultats',
setup: () => {
this.triggerIncomingCall({
numero: '+33688776655',
nom: 'Paul BERNARD',
centreId: 'centre4',
priority: 'low',
motif: 'Demande résultats radiologie',
examDate: '2024-12-20'
});
}
}
];
return scenarios;
}
// Interface de test pour le développement
showTestPanel() {
const testPanel = document.createElement('div');
testPanel.id = 'cti-test-panel';
testPanel.className = 'cti-test-panel';
testPanel.innerHTML = `
<h3>Panneau de test CTI</h3>
<div class="test-controls">
<button onclick="ctiSimulator.generateRandomCall()">Appel aléatoire</button>
<button onclick="ctiSimulator.startAutoSimulation(30)">Démarrer auto (30s)</button>
<button onclick="ctiSimulator.stopAutoSimulation()">Arrêter auto</button>
</div>
<div class="test-scenarios">
<h4>Scénarios</h4>
${this.simulateScenarios().map(s =>
`<button onclick="ctiSimulator.runScenario('${s.name}')">${s.name}</button>`
).join('')}
</div>
<div class="test-stats">
<h4>Statistiques</h4>
<div id="cti-stats"></div>
</div>
`;
// Ajouter les styles
if (!document.getElementById('cti-test-styles')) {
const styles = document.createElement('style');
styles.id = 'cti-test-styles';
styles.innerHTML = `
.cti-test-panel {
position: fixed;
right: 20px;
bottom: 20px;
width: 300px;
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 9999;
}
.cti-test-panel h3 {
margin-bottom: 10px;
color: #333;
font-size: 16px;
}
.cti-test-panel h4 {
margin: 10px 0 5px 0;
color: #666;
font-size: 14px;
}
.test-controls button,
.test-scenarios button {
margin: 5px;
padding: 5px 10px;
background: #667eea;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.test-controls button:hover,
.test-scenarios button:hover {
background: #5a6fd8;
}
#cti-stats {
font-size: 12px;
color: #666;
}
`;
document.head.appendChild(styles);
}
document.body.appendChild(testPanel);
this.updateStats();
}
// Exécuter un scénario
runScenario(name) {
const scenario = this.simulateScenarios().find(s => s.name === name);
if (scenario) {
scenario.setup();
}
}
// Mettre à jour les statistiques
updateStats() {
const stats = this.getCallStats();
const statsDiv = document.getElementById('cti-stats');
if (statsDiv) {
statsDiv.innerHTML = `
<div>Total: ${stats.total} appels</div>
<div>Aujourd'hui: ${stats.today}</div>
<div>Répondus: ${stats.answered}</div>
<div>Manqués: ${stats.missed}</div>
<div>Durée moy: ${stats.average_duration}s</div>
`;
}
}
}
// Créer une instance globale
const ctiSimulator = new CTISimulator();
// Exposer pour les tests en développement
// Dans le contexte renderer, vérifier si on est en mode développement via une variable globale
window.ctiSimulator = ctiSimulator;
// Afficher le panneau de test si en mode développement
ipcRenderer.invoke('is-development').then(isDev => {
if (isDev) {
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
const mainPage = document.getElementById('mainPage');
if (mainPage && mainPage.classList.contains('active')) {
ctiSimulator.showTestPanel();
}
}, 2000);
});
}
});
// Démarrer la simulation automatique après connexion (optionnel)
ipcRenderer.on('agent-logged-in', () => {
// Démarrer la simulation après 5 secondes
setTimeout(() => {
ctiSimulator.startAutoSimulation(90); // Un appel toutes les 90 secondes
}, 5000);
});
// Arrêter la simulation à la déconnexion
ipcRenderer.on('agent-logged-out', () => {
ctiSimulator.stopAutoSimulation();
});

View File

@@ -1,534 +0,0 @@
# Guide d'intégration SignalR pour SimpleConnect
## Vue d'ensemble
Ce document décrit l'architecture SignalR utilisée dans SimpleConnect v2 pour implémenter une intégration CTI (Computer Telephony Integration) complète avec gestion en temps réel des appels et basculement automatique entre les différentes plateformes de prise de rendez-vous.
## Architecture générale
### Composants principaux
1. **Serveur SignalR** : Hub centralisé gérant les événements téléphoniques
2. **Client Electron** : Application agent avec connexion persistante au hub
3. **Système IPBX** : Infrastructure téléphonique envoyant les événements
### Configuration serveur
```json
{
"ServerIp": "10.90.20.201:8002",
"ServiceProvider": "RDVPREM"
}
```
## Installation des dépendances
```bash
npm install @microsoft/signalr
```
## Implémentation du client SignalR
### 1. Connexion au Hub
```javascript
const signalR = require("@microsoft/signalr");
// Créer la connexion au hub SignalR
function createSignalRConnection(serverUrl) {
const connection = new signalR.HubConnectionBuilder()
.withUrl(`http://${serverUrl}/signalR`)
.withAutomaticReconnect([0, 2000, 5000, 10000]) // Reconnexion automatique
.configureLogging(signalR.LogLevel.Information)
.build();
return connection;
}
// Démarrer la connexion
async function startConnection(connection) {
try {
await connection.start();
console.log('Connexion SignalR établie');
return true;
} catch (err) {
console.error('Erreur de connexion SignalR:', err);
setTimeout(() => startConnection(connection), 5000);
return false;
}
}
```
### 2. Authentification des agents
```javascript
// Structure de connexion agent
async function agentLogin(connection, credentials) {
try {
const result = await connection.invoke('AgentLogin',
credentials.email,
credentials.password,
credentials.terminal
);
if (result) {
// Stockage des informations agent
global.AgentConnectionInfo = {
accessCode: result.accessCode,
firstName: result.firstName,
lastName: result.lastName,
connList: result.connList // Liste des files/applications
};
// Traitement des URLs des applications
processApplicationUrls(result.connList);
return { success: true, data: result };
}
return { success: false, message: 'Échec de connexion' };
} catch (error) {
console.error('Erreur login:', error);
return { success: false, message: error.message };
}
}
// Traitement des URLs avec placeholders
function processApplicationUrls(connList) {
return connList.map(conn => {
let url = conn.applicationName;
// Remplacement des placeholders
url = url.replace("#CA#", conn.accessCode);
url = url.replace("#MP#", conn.password);
// Gestion des cas spécifiques
if (url === "pro.mondocteur.fr") {
url = "https://pro.mondocteur.fr/backoffice.do";
}
if (url === "pro.doctolib.fr") {
url = "https://pro.doctolib.fr/signin";
}
return {
id: conn.code,
url: url,
credentials: {
accessCode: conn.accessCode,
password: conn.password
}
};
});
}
```
### 3. Gestion des événements IPBX en temps réel
```javascript
// Écouter les événements téléphoniques
function setupIpbxEventListener(connection, mainWindow) {
connection.on('IpbxEvent', (name, args) => {
if (!args || !global.AgentConnectionInfo) return;
const event = args[0];
console.log(`Événement IPBX reçu:
Code: ${event.eventCode}
Terminal: ${event.terminal}
File: ${event.queueName}
`);
handleIpbxEvent(event, mainWindow);
});
}
// Traitement des événements
function handleIpbxEvent(event, mainWindow) {
switch (event.eventCode) {
case 1: // Appel décroché
handleCallPickedUp(event, mainWindow);
break;
case 2: // Appel raccroché
handleCallHungUp(event, mainWindow);
break;
default:
console.log('Code événement non géré:', event.eventCode);
}
}
// Basculement automatique vers la bonne file
function handleCallPickedUp(event, mainWindow) {
// Identifier le centre correspondant à la file
const centre = identifyCentreByQueue(event.queueName);
if (centre) {
// Envoyer l'instruction de basculement à la fenêtre principale
mainWindow.webContents.send('switchToCenter', {
centreId: centre.id,
queueName: event.queueName,
terminal: event.terminal
});
// Logger l'événement
logCallEvent('CALL_PICKUP', event);
}
}
// Libération de la file après raccrochage
function handleCallHungUp(event, mainWindow) {
mainWindow.webContents.send('releaseCenter', {
queueName: event.queueName
});
// Logger l'événement
logCallEvent('CALL_HANGUP', event);
}
```
### 4. Récupération et sélection des terminaux téléphoniques
#### Processus complet de récupération des terminaux
La récupération des terminaux se fait en deux phases : récupération depuis le serveur et affichage dans l'interface de connexion.
##### Côté processus principal (main.js)
```javascript
// Handler IPC pour récupérer les terminaux
ipcMain.handle('get-terminal-list', async () => {
try {
const terminals = await signalRConnection.invoke(
'GetTerminalListByServiceProvider',
config.ServiceProvider // Ex: 'RDVPREM' depuis la config
);
console.log('Terminaux disponibles:', terminals);
// Retourne un tableau de numéros: ["3001", "3002", "3003", ...]
return terminals;
} catch (error) {
console.error('Erreur récupération terminaux:', error);
return [];
}
});
```
##### Côté renderer - Interface de sélection
```javascript
// Au chargement de la page de connexion
document.addEventListener('DOMContentLoaded', async () => {
try {
// Afficher un loader
const terminalSelect = document.getElementById('terminal-select');
terminalSelect.innerHTML = '<option value="">Chargement des terminaux...</option>';
// Récupérer les terminaux disponibles depuis le serveur
const terminals = await ipcRenderer.invoke('get-terminal-list');
if (terminals && terminals.length > 0) {
populateTerminalSelect(terminals);
// Restaurer la dernière sélection si disponible
const lastTerminal = localStorage.getItem('last-terminal');
if (lastTerminal && terminals.includes(lastTerminal)) {
terminalSelect.value = lastTerminal;
}
} else {
showError('Aucun terminal téléphonique disponible');
terminalSelect.innerHTML = '<option value="">Aucun terminal disponible</option>';
}
} catch (error) {
console.error('Erreur chargement terminaux:', error);
showError('Impossible de récupérer la liste des terminaux');
}
});
// Remplir le select avec les terminaux
function populateTerminalSelect(terminals) {
const select = document.getElementById('terminal-select');
select.innerHTML = '<option value="">Sélectionner un terminal...</option>';
terminals.forEach(terminal => {
const option = document.createElement('option');
option.value = terminal;
option.textContent = `Poste ${terminal}`;
select.appendChild(option);
});
}
// Intégration dans le formulaire de connexion
async function handleLogin(e) {
e.preventDefault();
const terminal = document.getElementById('terminal-select').value;
// Validation : terminal obligatoire
if (!terminal) {
showError('Veuillez sélectionner un terminal téléphonique');
return;
}
const credentials = {
email: document.getElementById('email').value,
password: document.getElementById('password').value,
terminal: terminal
};
// Sauvegarder le terminal pour la prochaine connexion
localStorage.setItem('last-terminal', terminal);
// Connexion avec le terminal sélectionné
const result = await ipcRenderer.invoke('agent-login', credentials);
if (result.success) {
// Le terminal est maintenant associé à la session
// Il sera affiché dans le titre de la fenêtre
console.log(`Connecté sur le poste ${terminal}`);
}
}
```
##### Interface HTML correspondante
```html
<!-- Formulaire de connexion avec sélection du terminal -->
<form id="loginForm">
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" required>
</div>
<div class="form-group">
<label for="password">Mot de passe</label>
<input type="password" id="password" required>
</div>
<div class="form-group">
<label for="terminal-select">Terminal téléphonique</label>
<select id="terminal-select" required>
<option value="">Chargement...</option>
</select>
</div>
<button type="submit">Se connecter</button>
</form>
```
#### Points importants sur les terminaux
1. **ServiceProvider** : Le paramètre configuré dans `config.json` détermine quels terminaux sont disponibles pour l'organisation
2. **Filtrage serveur** : Le serveur filtre automatiquement les terminaux selon :
- Le fournisseur de service (ServiceProvider)
- Les droits de l'organisation
- La disponibilité des postes
- Les terminaux déjà utilisés par d'autres agents
3. **Validation obligatoire** : Un agent DOIT sélectionner un terminal pour se connecter - c'est ce qui permet le routage des appels
4. **Affichage contextuel** : Après connexion, le terminal est affiché dans le titre :
```javascript
mainWindow.setTitle(`SimpleConnect - Agent: ${user} (${firstName} ${lastName}) - Tel: ${terminal}`);
```
5. **Association terminal-agent** : Cette association permet au serveur SignalR de router les événements IPBX vers le bon agent
### 5. Déconnexion propre
```javascript
// Déconnexion agent
async function agentLogoff(connection, userId) {
try {
await connection.invoke('AgentLogoff', userId);
console.log('Agent déconnecté');
// Nettoyer les données locales
global.AgentConnectionInfo = null;
} catch (error) {
console.error('Erreur déconnexion:', error);
}
}
// Arrêt de la connexion SignalR
async function stopConnection(connection) {
if (connection.state === signalR.HubConnectionState.Connected) {
await connection.stop();
console.log('Connexion SignalR fermée');
}
}
```
## Intégration dans l'application Electron
### Dans le processus principal (main.js)
```javascript
const { app, BrowserWindow, ipcMain } = require('electron');
const signalR = require("@microsoft/signalr");
let signalRConnection = null;
let mainWindow = null;
app.on('ready', async () => {
// Créer la fenêtre principale
mainWindow = createMainWindow();
// Charger la configuration
const config = loadConfig();
// Initialiser SignalR
signalRConnection = createSignalRConnection(config.ServerIp);
// Configurer les listeners
setupIpbxEventListener(signalRConnection, mainWindow);
// Démarrer la connexion
const connected = await startConnection(signalRConnection);
if (connected) {
mainWindow.webContents.send('signalr-connected');
}
});
// Gestion des IPC depuis le renderer
ipcMain.handle('agent-login', async (event, credentials) => {
return await agentLogin(signalRConnection, credentials);
});
ipcMain.handle('get-terminals', async (event) => {
return await getTerminalList(signalRConnection, config.ServiceProvider);
});
// Nettoyage à la fermeture
app.on('before-quit', async () => {
if (global.AgentConnectionInfo) {
await agentLogoff(signalRConnection, global.AgentConnectionInfo.accessCode);
}
await stopConnection(signalRConnection);
});
```
### Dans le processus renderer (renderer.js)
```javascript
const { ipcRenderer } = require('electron');
// Écouter la connexion SignalR
ipcRenderer.on('signalr-connected', () => {
console.log('SignalR connecté');
updateConnectionStatus('connected');
});
// Écouter les changements de file
ipcRenderer.on('switchToCenter', (event, data) => {
// Basculer automatiquement vers le bon centre
selectCenter(data.centreId);
// Afficher une notification
showNotification(`Appel entrant sur ${data.queueName}`);
// Mettre à jour l'interface
updateCallStatus('in-call', data);
});
// Libération de file
ipcRenderer.on('releaseCenter', (event, data) => {
updateCallStatus('available');
// Optionnel : revenir à l'écran d'accueil
showHomeScreen();
});
```
## Flux de données complet
### Séquence d'un appel type
1. **Appel entrant** → Système IPBX
2. **Événement généré** → Serveur SignalR
3. **IpbxEvent transmis** → Client Electron (eventCode: 1)
4. **Identification de la file** → Mapping file/centre
5. **Basculement automatique** → Webview du centre concerné
6. **Agent traite l'appel** → Prise de RDV
7. **Fin d'appel** → Système IPBX
8. **IpbxEvent transmis** → Client Electron (eventCode: 2)
9. **Libération de la file** → Retour état disponible
## Monitoring et logs
```javascript
// Structure de log pour dashboard
function createDashboardLog(agentInfo, terminals, connection) {
return {
"PrestaConnect": {
"Ouverture": {
"Ouvert": "Oui",
"Date": new Date().toISOString(),
"IP_Client": getLocalIP(),
"IP_Serveur": config.ServerIp,
"Liste_Telephones": terminals
},
"Connexion": {
"Connecte": agentInfo ? "Oui" : "Non",
"Agent": agentInfo?.accessCode,
"Telephone": agentInfo?.terminal,
"Nom_Agent": `${agentInfo?.firstName} ${agentInfo?.lastName}`,
"Nombre_Files": agentInfo?.connList.length,
"Files": agentInfo?.connList
},
"SignalR": {
"État": connection.state,
"Reconnexions": connection.reconnectAttempts || 0
}
}
};
}
// Écriture du log
function writeDashboardLog(logData) {
const fs = require('fs');
fs.writeFileSync(
'./log/log-Dashboard.json',
JSON.stringify(logData, null, 2)
);
}
```
## Points d'attention pour l'implémentation
### 1. Gestion de la reconnexion
- Implémenter une stratégie de reconnexion automatique
- Gérer les états de connexion dans l'interface
- Sauvegarder l'état local pour restauration après reconnexion
### 2. Sécurité
- Ne jamais exposer les credentials en clair dans les logs
- Utiliser HTTPS pour la connexion SignalR en production
- Implémenter un timeout de session
### 3. Performance
- Limiter le nombre de listeners SignalR actifs
- Nettoyer les listeners lors des changements de contexte
- Implémenter un système de cache pour les données statiques
### 4. Gestion d'erreurs
- Capturer toutes les erreurs de connexion
- Afficher des messages utilisateur clairs
- Logger tous les événements pour debug
## Tests recommandés
1. **Test de connexion/déconnexion**
2. **Test de basculement automatique entre files**
3. **Test de perte de connexion réseau**
4. **Test de charge avec appels multiples**
5. **Test de synchronisation multi-agents**
## Prochaines étapes
1. Adapter le code SignalR au contexte de notre application
2. Configurer le serveur SignalR backend
3. Mapper les centres médicaux avec les files téléphoniques
4. Implémenter l'interface de monitoring temps réel
5. Tester l'intégration complète avec le système IPBX

View File

@@ -1,542 +0,0 @@
# Workflow complet d'un agent SimpleConnect
## Vue d'ensemble
Ce document décrit le parcours complet d'un agent utilisant SimpleConnect, depuis la connexion jusqu'à la déconnexion, en passant par la gestion des appels et l'utilisation des différentes fonctionnalités.
## 1. Lancement de l'application
### Actions au démarrage
```javascript
// L'agent lance l'application SimpleConnect
// Initialisation automatique des composants
```
**Processus système** :
- Connexion automatique au serveur SignalR (configuré dans `config.json`)
- Vérification de la disponibilité du service
- Récupération de la liste des terminaux téléphoniques disponibles
- Création du fichier de log journalier
- Génération du dashboard initial (état : application ouverte, non connecté)
## 2. Phase de connexion
### 2.1 Interface de connexion
L'agent arrive sur l'écran de connexion avec :
- Champ code agent
- Champ mot de passe
- Sélecteur de terminal téléphonique (liste dynamique)
- Option "Forcer la déconnexion" (checkbox)
- Bouton de connexion
### 2.2 Processus d'authentification
#### Connexion standard
```javascript
// Envoi des credentials au serveur
AgentLogin(email, password, terminal) Serveur SignalR
// Réponse du serveur avec les informations complètes
{
accessCode: "AGENT001",
firstName: "Marie",
lastName: "DUPONT",
terminal: "3001",
connList: [ // Centres/files assignés à l'agent
{
code: "centre1",
applicationName: "https://pro.doctolib.fr",
accessCode: "user_cardio",
password: "pass123"
},
{
code: "centre2",
applicationName: "https://pro.mondocteur.fr",
accessCode: "user_clinique",
password: "pass456"
}
// ...
]
}
```
#### Connexion avec déconnexion forcée
Utilisée dans les cas suivants :
- **Session bloquée** : Déconnexion anormale précédente (crash, perte réseau)
- **Changement de poste** : L'agent se connecte depuis un autre ordinateur
- **Reprise après erreur** : Nettoyage d'une session corrompue
```javascript
// Si la case "Forcer la déconnexion" est cochée
if (forceLogoff) {
// 1. D'abord déconnexion de la session précédente
AgentLogoff(userId) Serveur SignalR
// 2. Puis nouvelle connexion
AgentLogin(email, password, terminal) Serveur SignalR
}
```
**Interface HTML correspondante** :
```html
<form id="loginForm">
<input type="text" id="codeAgent" placeholder="Code agent" required>
<input type="password" id="password" placeholder="Mot de passe" required>
<select id="terminal" required>
<option value="">Sélectionner un terminal...</option>
<!-- Options dynamiques -->
</select>
<!-- Option de déconnexion forcée -->
<label class="checkbox-label">
<input type="checkbox" id="forceLogoff">
<span>Forcer la déconnexion de ma session précédente</span>
<small>Cochez si vous rencontrez des problèmes de connexion</small>
</label>
<button type="submit">Se connecter</button>
</form>
```
**Implémentation technique** :
```javascript
// Handler de connexion avec gestion de la déconnexion forcée
function handleLogin(event, forceDisconnect, user, password, terminal) {
try {
// Si déconnexion forcée demandée ET user fourni
if (forceDisconnect && user !== null) {
// Nettoyer la session précédente
AgentLogoff(user);
console.log(`Session précédente de ${user} fermée`);
}
// Procéder à la connexion normale
client.invoke('AgentLogin', user, password, terminal)
.then(result => {
if (result) {
// Connexion réussie
processSuccessfulLogin(result);
} else {
// Échec de connexion
showError('Identifiants incorrects');
}
});
} catch (error) {
console.error('Erreur lors de la connexion:', error);
}
}
```
Cette fonctionnalité garantit qu'un agent peut toujours reprendre son travail rapidement, même après une déconnexion anormale, sans nécessiter d'intervention technique.
### 2.3 Post-connexion
**Actions automatiques après connexion réussie** :
1. Génération dynamique des onglets selon les centres assignés
2. Création des webviews pour chaque plateforme
3. Auto-connexion aux plateformes médicales (injection des credentials)
4. Mise à jour de l'interface :
- Titre fenêtre : "SimpleConnect - Agent: Marie DUPONT - Tel: 3001"
- Nom agent affiché dans l'interface
- Statut : "Disponible" (indicateur vert)
5. Activation de l'écoute des événements IPBX
6. Mise à jour du dashboard JSON avec les informations de connexion
## 3. Phase d'activité - Gestion des appels
### 3.1 État disponible
**L'agent est prêt à recevoir des appels** :
- Statut : indicateur vert "Disponible"
- Peut naviguer librement entre les onglets
- Peut consulter les plannings de manière proactive
- Statistiques visibles : appels traités, RDV pris
### 3.2 Réception d'un appel entrant
#### Séquence d'événements
```
1. Patient appelle le centre
2. Système IPBX détecte l'appel
3. Serveur SignalR envoie IpbxEvent
4. Client reçoit : {eventCode: 1, terminal: "3001", queueName: "centre1"}
5. Interface réagit automatiquement
```
#### Actions système
**Notification immédiate** :
```javascript
// Alerte visuelle
showIncomingCallAlert({
centreName: "Centre Cardio Lyon",
patientInfo: "Marie LAMBERT - 0612345678",
motif: "Consultation urgente",
});
// Son de notification
playNotificationSound();
```
**Auto-acceptation** :
- Timer de 3 secondes avant acceptation automatique
- Ou acceptation manuelle immédiate par l'agent
#### Basculement automatique
```javascript
// Le système identifie le centre concerné
selectCenter("centre1");
// Bascule sur le bon onglet/webview
switchToTab("Centre Cardio Lyon");
// Mise à jour du statut
updateStatus("EN APPEL - Centre Cardio Lyon");
```
### 3.3 Pendant l'appel
**Actions possibles de l'agent** :
1. **Prise de RDV** :
- Navigation dans le planning actif
- Recherche de créneaux disponibles
- Validation du RDV directement dans la webview
2. **Prise de notes** :
```javascript
// Zone de notes rapides
quickNotes.value = "Patient souhaite mardi matin de préférence";
saveNotes(); // Sauvegarde horodatée
```
3. **Consultation documentation** :
```javascript
// Ouverture wiki interne (nouvelle fenêtre)
OpenDoc("http://wiki.interne/protocoles");
// Consultation infos patient (fenêtre séparée)
OpenDoc("http://crm.interne/patient/12345");
```
4. **Informations contextuelles** :
- Historique des derniers appels du patient (si disponible)
- Procédures spécifiques au centre
- Tarifs et modalités
### 3.4 Fin d'appel
#### Séquence de libération
```
1. Patient/Agent raccroche
2. Système IPBX détecte la fin d'appel
3. Serveur SignalR envoie IpbxEvent
4. Client reçoit : {eventCode: 2, terminal: "3001", queueName: "centre1"}
5. Libération automatique de la file
```
#### Actions post-appel
```javascript
// Mise à jour automatique
updateStatus("DISPONIBLE");
callStats.calls++;
callStats.appointments++; // Si RDV pris
// Sauvegarde dans l'historique
saveCallHistory({
timestamp: new Date().toISOString(),
duration: 180, // secondes
centre: "centre1",
status: "completed",
appointment: true,
});
```
## 4. Fonctionnalités disponibles pendant la session
### 4.1 Navigation multi-centres
```javascript
// L'agent peut gérer plusieurs centres simultanément
centres = [
{ id: "centre1", nom: "Centre Cardio Lyon", couleur: "#FF6B6B" },
{ id: "centre2", nom: "Clinique Saint-Jean", couleur: "#4ECDC4" },
{ id: "centre3", nom: "Cabinet Dr Martin", couleur: "#45B7D1" },
];
// Navigation libre entre les onglets (hors appel)
selectCenter("centre2"); // Changement manuel
```
### 4.2 Outils de productivité
**Statistiques temps réel** :
```javascript
{
calls: 15, // Appels traités aujourd'hui
appointments: 12, // RDV confirmés
avgDuration: 180, // Durée moyenne en secondes
successRate: 80 // Taux de conversion
}
```
**Notes et historique** :
```javascript
// Notes rapides par appel
saveNotes({
content: "Patient à rappeler pour confirmation",
centre: "centre1",
timestamp: new Date(),
});
// Consultation historique
getCallHistory(); // 100 derniers appels
```
### 4.3 Fenêtres auxiliaires
```javascript
// Documentation (DocXplore)
childClientDocXplore = new BrowserWindow({
x: 1920,
y: 0, // Position configurée
title: "SimpleConnect / Wiki",
});
// Informations client
childClientDoc = new BrowserWindow({
x: 2880,
y: 0, // Position configurée
title: "SimpleConnect / Infos client",
});
```
## 5. Processus de déconnexion
### 5.1 Déclenchement
La déconnexion peut être initiée par :
- Clic sur le bouton "Déconnexion"
- Fermeture de l'application (croix de fenêtre)
- Timeout de session (si configuré)
### 5.2 Confirmation et nettoyage
```javascript
// Dialogue de confirmation
if (confirm("Voulez-vous vraiment vous déconnecter ?")) {
// 1. Notification au serveur
AgentLogoff(agentId);
// 2. Nettoyage local
currentAgent = null;
currentCentres = [];
webviews = {};
// 3. Fermeture des fenêtres auxiliaires
if (childClientDocXplore) childClientDocXplore.close();
if (childClientDoc) childClientDoc.close();
// 4. Génération log final
writeDashboardLog({
PrestaConnect: {
Connexion: {
Connecte: "Non",
Date_Deconnexion: new Date().toISOString(),
},
},
});
// 5. Retour page connexion ou fermeture
showLoginPage(); // ou app.quit()
}
```
## 6. Logs et monitoring
### 6.1 Structure du dashboard
**Fichier** : `./log/log-Dashboard.json`
```json
{
"PrestaConnect": {
"Ouverture": {
"Ouvert": "Oui",
"Date": "2024-12-04T10:00:00.000Z",
"IP_Client": "192.168.1.50",
"IP_Serveur": "10.90.20.201:8002",
"Liste_Telephones": ["3001", "3002", "3003"]
},
"Connexion": {
"Connecte": "Oui",
"Date": "2024-12-04T10:01:00.000Z",
"Agent": "AGENT001",
"Telephone": "3001",
"Nom_Agent": "Marie DUPONT",
"Nombre_Files": 3,
"Files": [
{
"Nom_File": "centre1",
"URL_File": "https://pro.doctolib.fr"
},
{
"Nom_File": "centre2",
"URL_File": "https://pro.mondocteur.fr"
}
]
}
}
}
```
### 6.2 Logs détaillés
**Fichier** : `./log/YYYY-MM-DD-log.txt`
```
[2024-12-04 10:00:00.123] [info] Start application
[2024-12-04 10:00:01.456] [info] ip: 10.90.20.201:8002
[2024-12-04 10:00:02.789] [info] SignalR connected
[2024-12-04 10:01:00.123] [info] Agent login: AGENT001
[2024-12-04 10:05:30.456] [info] IpbxEvent received: eventCode=1, terminal=3001
[2024-12-04 10:08:45.789] [info] IpbxEvent received: eventCode=2, terminal=3001
```
## 7. Mode développement et simulation
### 7.1 Panneau de test CTI
En mode développement (`NODE_ENV=development`), un panneau de test permet :
```javascript
// Simulation d'appels
ctiSimulator.generateRandomCall();
// Simulation automatique
ctiSimulator.startAutoSimulation(30); // Un appel toutes les 30 secondes
// Scénarios prédéfinis
ctiSimulator.simulateScenarios():
- "Appel urgent"
- "Patient régulier"
- "Nouveau patient"
- "Demande résultats"
```
### 7.2 Statistiques de simulation
```javascript
{
total: 45, // Total appels simulés
today: 15, // Appels aujourd'hui
answered: 14, // Appels traités
missed: 1, // Appels manqués
average_duration: 180 // Durée moyenne
}
```
## 8. Diagramme d'états
```
┌─────────────┐
│ DÉCONNECTÉ │
└─────┬───────┘
│ Connexion
┌─────────────┐
│ CONNEXION │
└─────┬───────┘
│ Authentification réussie
┌─────────────┐ Appel entrant ┌─────────────┐
│ DISPONIBLE │ ←─────────────────→ │ EN APPEL │
└─────┬───────┘ Fin d'appel └─────────────┘
│ Déconnexion
┌─────────────┐
│ DÉCONNEXION │
└─────────────┘
```
## 9. Points clés du workflow
### Avantages pour l'agent
1. **Zéro friction** : Pas de recherche manuelle de plateforme
2. **Contexte automatique** : Basculement instantané vers le bon planning
3. **Productivité maximale** : Auto-connexion, notes rapides, historique
4. **Traçabilité complète** : Tous les événements sont enregistrés
5. **Multi-tâches optimisé** : Gestion simultanée de plusieurs centres
### Optimisations système
1. **Reconnexion automatique** SignalR en cas de perte réseau
2. **Sessions persistantes** dans les webviews
3. **Cache local** des dernières sélections (terminal, notes)
4. **Logs structurés** pour analyse et monitoring
5. **Mode simulation** pour formation et tests
## 10. Cas d'usage spéciaux
### 10.1 Appel urgent
```javascript
// Identification par priorité haute
if (callData.priority === "high") {
// Notification visuelle renforcée
showUrgentCallAlert(callData);
// Son différent
playUrgentSound();
// Auto-acceptation immédiate
acceptCall(callData);
}
```
### 10.2 Patient régulier
```javascript
// Affichage historique patient
if (callData.lastVisit) {
showPatientHistory({
lastVisit: callData.lastVisit,
appointments: callData.appointmentHistory,
});
}
```
### 10.3 Multi-files simultanées
L'agent peut recevoir des appels de différentes files et le système bascule automatiquement vers la bonne plateforme à chaque appel, permettant une gestion fluide multi-centres.

View File

@@ -1,679 +0,0 @@
# Changelog - SimpleConnect Electron
## [1.4.1] - 2025-10-21
### Ajouté
- **Bouton "Quitter" sur la page de connexion** : Permet de fermer l'application sans se connecter
- Bouton gris positionné sous le bouton "Se connecter"
- Fermeture propre de l'application via IPC `quit-app`
- Style secondaire pour différenciation visuelle du bouton principal
### Technique
- Nouveau style CSS `.btn-quit` avec couleur grise et effet hover
- Fonction `handleQuitFromLogin()` pour gérer la fermeture depuis la page de login
- Utilisation du handler IPC existant `quit-app`
## [1.4.0] - 2025-10-21
### Ajouté
- **Affichage de la version dans l'interface** : La version de l'application s'affiche désormais clairement
- Version affichée sous le titre "SimpleConnect" sur la page de connexion (centrée, gris clair)
- Version affichée dans le header de la page principale à côté du logo
- Handler IPC `get-app-version` pour exposer la version du package.json
- Injection automatique de la version au chargement de l'application
### Modifié
- **Titre de la fenêtre avec version** : Le titre de la barre native affiche maintenant "SimpleConnect vX.X.X"
- Titre initial défini dans BrowserWindow avec la version
- Titre mis à jour dynamiquement lors de la connexion/déconnexion agent
- Listener `did-finish-load` pour forcer le titre après chargement du HTML
- Format : "SimpleConnect v1.4.0 - Agent: XXX - Tel: XXX" quand connecté
### Technique
- Nouveau style CSS `.app-version-login` pour l'affichage de la version sur la page de login
- Style CSS `.app-version` pour l'affichage dans le header principal
- Utilisation de `app.getVersion()` pour récupérer la version depuis package.json
- Résolution du conflit entre `<title>` HTML et titre de BrowserWindow
## [1.3.1] - 2025-10-17
### Ajouté
- **Saisie manuelle de postes téléphoniques personnalisés** : L'utilisateur peut désormais taper un numéro de poste qui n'est pas dans la liste officielle
- Activation de `addItems` et `addChoices` dans Choices.js pour les éléments `<select>`
- Validation de format numérique uniquement
- Avertissement visuel non-bloquant (5 secondes) si le poste n'est pas dans la liste officielle
- Textes UX explicites guidant l'utilisateur ("Rechercher ou saisir un poste...")
- Le serveur accepte tout numéro de poste valide
### Modifié
- **Affichage des terminaux simplifié** : Les numéros de postes s'affichent maintenant sans le préfixe "Poste"
- Affichage direct "2101" au lieu de "Poste 2101"
- Interface plus épurée et compacte
- Modifié dans Choices.js et dans tous les fallbacks natifs
### Corrigé
- **Contraste de l'élément survolé dans la liste déroulante** : Amélioration de la lisibilité
- Fond bleu clair (#e8f0fe) au lieu de bleu foncé (#667eea)
- Texte noir foncé (#1a1a1a) au lieu de blanc
- Poids de police semi-gras (500) pour meilleure lisibilité
- Résout le problème d'illisibilité lors de la navigation au clavier
## [1.3.0] - 2025-09-12
### Ajouté
- **Support dual SignalR/SocketIO avec fallback automatique** : Compatibilité totale avec backends .NET et Python
- ConnectionManager qui essaie d'abord SignalR puis bascule sur SocketIO
- WebSocketAdapter qui émule l'API SignalR complète avec socket.io-client
- Abstraction totale : même API peu importe le protocole utilisé
- Détection automatique du type de serveur disponible
- Messages de statut indiquant le type de connexion active (SignalR ou WebSocket)
### Modifié
- **Architecture de connexion refactorisée** : Système modulaire avec adaptateurs
- Nouveau module `connection-manager.js` pour gérer la stratégie de fallback
- Nouveau module `websocket-adapter.js` pour l'émulation SignalR avec SocketIO
- Code principal simplifié grâce à l'abstraction de connexion
- Meilleure gestion des erreurs et reconnexion automatique
### Technique
- Ajout de la dépendance `socket.io-client` v4.8.1
- Pattern Adapter pour unifier les APIs SignalR et SocketIO
- Gestion des promesses pour les invocations asynchrones
- Mapping automatique des événements entre les deux protocoles
- Conservation de la compatibilité ascendante avec les serveurs existants
### Documentation
- Support confirmé pour les backends Python/FastAPI avec SocketIO
- Migration transparente entre serveurs .NET et Python
- Logs détaillés du type de connexion utilisé
## [1.2.16] - 2025-09-05
### Ajouté
- **Système de logging SignalR complet** : Capture et analyse de tous les événements
- Fichier de log centralisé dans `~/.simpleconnect-ng/signalr.log`
- Logger universel pour tous les messages SignalR reçus
- Écoute de 13 types d'événements potentiels (IpbxEvent, AgentStatusChanged, QueueUpdate, etc.)
- Format JSON structuré avec timestamp, arguments et contexte agent
- Logs des méthodes invoquées (AgentLogin, AgentLogoff, GetTerminalListByServiceProvider)
- Identification des codes IPBX 0-5 avec descriptions détaillées
### Corrigé
- **Icônes manquantes sur Linux** : Remplacement des emojis par des SVG
- Icônes SVG inline pour les boutons Rafraîchir et Notes
- Compatibilité universelle (Windows, Mac, Linux)
- Style adaptatif suivant le thème (currentColor)
- Animations au survol et lors des actions
- **Barre de menu Electron** : Suppression complète sur tous les OS
- Ajout de `autoHideMenuBar: true` dans BrowserWindow
- `setMenuBarVisibility(false)` pour forcer la suppression
- `Menu.setApplicationMenu(null)` pour suppression globale
- Interface épurée sans menu "File, Edit, View, Window, Help"
### Modifié
- **Configuration de build Linux** : Support multi-architectures
- Ajout des cibles AppImage, .deb et .rpm
- Support x64 et arm64
- Scripts npm dédiés : `build:linux-x64` et `build:linux-arm64`
- Métadonnées Linux enrichies (maintainer, vendor, synopsis)
### Technique
- Module `os` ajouté pour accès au répertoire home utilisateur
- Fonctions de logging : `ensureLogDirectory()`, `logToSignalRFile()`, `logSignalR()`
- CSS pour icônes SVG avec transitions et animations
- Build cross-platform depuis Mac M1 vers Linux AMD64
### Documentation
- Instructions complètes pour le build Linux
- Guide d'utilisation des fichiers AppImage, .deb et .rpm
- Explication du poids des fichiers AppImage (106 MB)
## [1.2.15] - 2025-09-04
### Corrigé
- **Position des notifications** : Les bandeaux de notification ne cachent plus les boutons
- Décalage vertical de 20px à 70px pour apparaître sous la barre d'outils
- Les boutons restent accessibles pendant l'affichage des notifications
### Modifié
- **Bouton de déconnexion remplacé par "Quitter"** : Changement du comportement de fermeture
- Le bouton "Déconnecter" devient "Quitter" pour plus de clarté
- Déconnexion automatique de l'agent avant fermeture
- Animation de déconnexion maintenue pour une transition fluide
- Fermeture propre de la connexion SignalR
- Arrêt complet de l'application après déconnexion
### Technique
- Nouveau handler IPC `quit-app` pour fermer l'application
- Modification de `handleConfirm()` pour appeler la fermeture après déconnexion
- Délai de 1 seconde avant fermeture pour voir l'animation complète
### Documentation
- **README.md entièrement réécrit** : Mise à jour complète de la documentation
- Ajout des fonctionnalités actuelles (SignalR, CTI, panneau de notes)
- Architecture technique détaillée et à jour
- Workflow d'utilisation complet
- Section dépannage avec problèmes courants
- Roadmap des fonctionnalités futures
- Structure moderne avec emojis et organisation claire
### Configuration
- **Nettoyage complet du config.json** : Suppression des éléments obsolètes
- Suppression de la section agents (maintenant géré via SignalR)
- Suppression de la section centres (fournis par le serveur)
- Suppression de la section CTI (ancien système de simulation)
- Suppression de la section preferences (non utilisée)
- Conservation uniquement de la configuration SignalR essentielle
- Fichier réduit de 118 lignes à 8 lignes
## [1.2.14] - 2025-09-04
### Ajouté
- **Système de persistance des notes amélioré** : Sauvegarde et restauration automatiques
- Auto-save après 2 secondes d'inactivité
- Restauration automatique des notes au démarrage
- Synchronisation localStorage + fichier serveur
- Notification visuelle lors de la restauration
- Historique local des 20 dernières notes dans localStorage
### Modifié
- **Gestion des fichiers de notes** : Un seul fichier par agent au lieu de multiples
- Format `notes_{agentId}.json` unique par agent
- Mise à jour du même fichier à chaque sauvegarde
- Historique des 50 dernières versions intégré dans le fichier
- Plus d'accumulation de fichiers datés
### Amélioré
- **Expérience utilisateur des notes** : Persistance transparente
- Chargement prioritaire depuis le fichier serveur
- Fallback sur localStorage si fichier absent
- Bouton "Effacer" vide aussi localStorage
- Messages de confirmation et notifications
### Technique
- Nouvelle fonction `loadSavedNotes()` asynchrone
- Handler IPC `get-notes` pour récupérer depuis le serveur
- Auto-save avec debouncing de 2 secondes
- Structure JSON avec note courante + historique
## [1.2.13] - 2025-09-04
### Corrigé
- **Panneau de notes partiellement visible** : Correction du bug d'affichage au démarrage
- Le panneau était partiellement visible même fermé
- Position cachée ajustée à -620px pour garantir l'invisibilité complète
- Gestion dynamique du décalage selon la largeur actuelle
- Réinitialisation explicite de la position à l'ouverture
### Amélioré
- **Indicateur de redimensionnement plus visible** : Meilleure visibilité des barres
- Zone de clic élargie à 16px pour faciliter la saisie
- 3 barres verticales créées avec gradient CSS au lieu de caractères
- Hauteur de 30px pour une meilleure visibilité
- Changement de couleur gris vers violet au survol
## [1.2.12] - 2025-09-04
### Ajouté
- **Redimensionnement du panneau de notes** : Possibilité d'ajuster la largeur
- Poignée de redimensionnement sur le bord gauche du panneau
- Indicateur visuel permanent (3 points verticaux)
- Largeur minimale : 280px, maximale : 600px
- Sauvegarde automatique de la largeur préférée dans localStorage
- Restauration de la largeur à la réouverture
### Amélioré
- **Fluidité du redimensionnement** : Optimisations pour une meilleure performance
- Utilisation de requestAnimationFrame pour 60 FPS
- Overlay invisible pendant le drag pour capturer tous les mouvements
- Désactivation des transitions CSS pendant le redimensionnement
- Indicateur visuel toujours visible (pas seulement au survol)
- Changement de couleur de l'indicateur au survol (gris → violet)
### Technique
- Variable CSS `--notes-width` pour synchroniser panneau et webview
- Classes `.resizing` pour désactiver les transitions pendant le drag
- Gestion des événements mouse avec requestAnimationFrame
- Limites de taille avec Math.min/max pour contraindre la largeur
## [1.2.11] - 2025-09-04
### Ajouté
- **Panneau de notes latéral moderne** : Refonte complète de l'interface des notes
- Panneau qui glisse depuis la droite de l'écran (au lieu du bas)
- Header avec gradient violet et icône intégrée
- Placeholder avec suggestions d'utilisation
- Nouveau bouton "Effacer" pour réinitialiser les notes
- Footer dédié pour les actions
- Animation fluide cubic-bezier pour l'ouverture/fermeture
### Modifié
- **Design du panneau de notes** : Interface plus moderne et cohérente
- Hauteur complète (100vh - 60px) au lieu d'une fenêtre popup
- Position proche du bouton d'activation pour meilleure cohérence
- Bouton de fermeture circulaire avec effet hover
- Textarea avec fond grisé qui devient blanc au focus
- Notifications modernes remplaçant les alert() natifs
- Webview qui se redimensionne automatiquement quand les notes sont ouvertes
### Supprimé
- **Badge de notification rouge** : Suppression de l'indicateur de contenu
- Plus de point rouge sur le bouton des notes
- Suppression du code JavaScript de surveillance du contenu
- Interface plus épurée sans distractions visuelles
- **Code du bouton flottant** : Nettoyage du CSS non utilisé
### Technique
- Nouvelle classe `.notes-open` pour ajuster la webview
- Animation `slideInRight` pour l'apparition du panneau
- Fonction `clearNotes()` avec confirmation avant effacement
- Tooltips modernes sur les boutons avec pseudo-éléments CSS
## [1.2.10] - 2025-09-04
### Modifié
- **Animation de connexion unifiée** : Messages identiques pour tous les types de connexion
- Suppression de la différenciation entre connexion normale et avec déblocage
- Même expérience visuelle que la case "Débloquer" soit cochée ou non
- Messages simplifiés : "Connexion en cours..." / "Authentification auprès du serveur"
- Interface plus cohérente et moins confuse pour l'utilisateur
### Technique
- Suppression de la condition sur `isForceDisconnect` dans `showLoginProgress()`
- Uniformisation des textes d'animation pour tous les cas de connexion
## [1.2.9] - 2025-09-04
### Ajouté
- **Autofocus sur le champ code d'accès** : Amélioration de l'ergonomie de connexion
- Focus automatique au lancement de l'application
- Focus automatique après déconnexion
- Sélection automatique du contenu existant pour remplacement rapide
- Plus besoin de cliquer avant de taper
### Modifié
- **Expérience utilisateur de connexion** : Accès direct au clavier
- Attribut HTML `autofocus` sur le champ code d'accès
- Focus forcé après fermeture de la modal de déconnexion
- Délais optimisés pour garantir le focus malgré les animations
### Technique
- Focus appliqué à 3 endroits : attribut HTML, chargement initial, après déconnexion
- Utilisation de `select()` pour sélectionner le contenu existant
- Délais de 100-200ms pour contourner les interférences des animations
## [1.2.8] - 2025-09-04
### Ajouté
- **Animation de connexion** : Feedback visuel pendant le processus d'authentification
- Modal avec spinner animé identique à celui de déconnexion
- Messages dynamiques adaptés au contexte (connexion normale ou forcée)
- Progression en plusieurs étapes : authentification → chargement des centres
- Design cohérent avec la modal de déconnexion
### Modifié
- **Expérience de connexion** : Interface plus professionnelle
- Plus d'attente sans feedback visuel
- Messages clairs sur l'état du processus
- Transitions fluides entre les étapes
- Modal automatiquement fermée en cas d'erreur ou de succès
### Technique
- Nouvelles fonctions : showLoginProgress(), updateLoginProgress(), hideLoginProgress()
- Styles CSS pour .login-progress-modal avec animation scaleIn
- Gestion asynchrone avec délais pour transitions fluides
- Messages adaptés selon le mode de connexion (normal/forcé)
## [1.2.7] - 2025-09-04
### Ajouté
- **Animation de déconnexion** : Feedback visuel pendant le processus
- Spinner circulaire animé avec rotation fluide (1 tour/seconde)
- Design cohérent avec le thème violet de l'application
- Ombre subtile pour un effet de profondeur
- Transition en fondu entre l'icône et le spinner
### Modifié
- **Modal de déconnexion** : États visuels dynamiques
- Textes mis à jour pendant la déconnexion ("Déconnexion en cours...", "Veuillez patienter")
- Boutons masqués pendant le processus pour éviter les doubles clics
- Délais optimisés : 300ms avant + 500ms après pour une transition fluide
- Restauration automatique de l'état initial après déconnexion
### Technique
- Nouvelles fonctions showLogoutProgress() et hideLogoutProgress()
- Animation CSS @keyframes spin pour la rotation du spinner
- Gestion asynchrone avec Promise pour les délais d'animation
- Structure HTML modifiée avec IDs pour contrôle dynamique
## [1.2.6] - 2025-09-04
### Ajouté
- **Tri alphabétique des onglets** : Organisation logique des centres
- Les onglets sont maintenant triés par ordre alphabétique du code centre
- Utilisation de `localeCompare()` avec locale français
- Option `numeric: true` pour gérer correctement les codes avec nombres (ACR2 avant ACR10)
### Modifié
- **Ordre d'affichage des centres** : Prévisible et cohérent
- Indépendant de l'ordre retourné par SignalR
- Premier onglet sélectionné = premier alphabétiquement
- Plus facile de retrouver un centre spécifique
### Technique
- Création d'une copie triée avec `[...currentCentres].sort()`
- Tri appliqué dans `initializeCenters()` et `showMainPage()`
- Préservation du tableau original pour les autres fonctionnalités
## [1.2.5] - 2025-09-04
### Corrigé
- **Écran blanc à la connexion** : Sélection automatique du premier onglet
- Plus d'écran vide après connexion
- Le premier planning s'affiche automatiquement
- L'onglet correspondant est marqué comme actif
### Modifié
- **Expérience de connexion** : Accès direct au premier planning
- Ajout de `selectCenter(currentCentres[0].id)` dans showMainPage()
- Suppression du message "Sélectionnez un centre ou attendez un appel entrant"
- Transition immédiate vers le contenu utile
### Technique
- Appel automatique de selectCenter() après initializeCenters()
- Suppression du div .no-center-selected du HTML
- Amélioration du flux utilisateur post-connexion
## [1.2.4] - 2025-09-04
### Corrigé
- **Bug du formulaire de connexion après déconnexion** : Réinitialisation complète
- Le bouton "Se connecter" restait grisé avec le texte "Reconnexion..." après déconnexion
- Les champs restaient pré-remplis avec les anciennes valeurs
- La checkbox "Débloquer" restait cochée
### Ajouté
- **Fonction resetLoginForm()** : Nettoyage du formulaire de connexion
- Vide automatiquement les champs code d'accès et mot de passe
- Décoche la checkbox "Débloquer"
- Efface les messages d'erreur
- Réactive le bouton et restaure le texte "Se connecter"
- Préserve la sélection du terminal pour la commodité
### Modifié
- **Comportement de déconnexion** : Expérience utilisateur améliorée
- Retour sur un formulaire de connexion propre et fonctionnel
- Plus d'état "bloqué" avec le bouton désactivé
- Réinitialisation également appliquée au démarrage si personne n'est connecté
## [1.2.3] - 2025-09-04
### Ajouté
- **Modal de déconnexion personnalisée** : Interface moderne pour la confirmation
- Design élégant avec icône emoji 👋 dans un cercle gradient
- Animation pulse sur l'icône et effet scaleIn à l'ouverture
- Textes en français avec titre et sous-titre descriptif
- Fond flou avec overlay sombre (backdrop-filter)
- Boutons stylisés avec gradient violet et effets hover
### Modifié
- **Expérience utilisateur de déconnexion** : Remplacement du confirm() natif
- Plus de popup système Electron avec logo générique
- Interface cohérente avec le design de l'application
- Trois méthodes de fermeture : bouton Annuler, clic externe, touche Escape
- Gestion propre des event listeners avec nettoyage automatique
### Technique
- Nouvelle fonction showLogoutModal() remplaçant le confirm() natif
- HTML de la modal ajouté avec structure sémantique
- Styles CSS avec animations @keyframes scaleIn
- Z-index 3000 pour s'assurer que la modal est au-dessus de tout
## [1.2.2] - 2025-09-04
### Supprimé
- **Barre d'outils des webviews** : Suppression complète de la toolbar
- Plus de boutons de navigation (Précédent/Suivant)
- Plus d'affichage de l'URL courante
- Plus de bouton Rafraîchir dans la toolbar
- Gain d'espace vertical supplémentaire (~40px)
- Code nettoyé : suppression de navigateWebview() et des event listeners associés
### Ajouté
- **Bouton Rafraîchir dans le header** : Nouvelle position pour le rafraîchissement
- Icône 🔄 ajoutée dans la zone droite du header
- Placé entre le statut de connexion et le bouton Notes
- Animation de rotation d'1 seconde lors du clic
- Fonction refreshCurrentWebview() pour rafraîchir uniquement la webview active
### Technique
- Suppression du code HTML de création de la toolbar
- Suppression de l'event listener 'did-navigate' pour l'URL
- Nouvelle animation CSS @keyframes rotate pour le bouton
- Classe .rotating pour l'animation visuelle du rafraîchissement
## [1.2.1] - 2025-09-04
### Modifié
- **Header et onglets fusionnés** : Optimisation de l'espace vertical
- Fusion du header et de la barre d'onglets sur une seule ligne
- Logo SimpleConnect et nom de l'agent à gauche
- Onglets des centres au milieu (espace flexible)
- Statut de connexion et boutons d'action à droite
- Gain de 10px en hauteur (60px au lieu de 70px)
### Amélioré
- **Interface plus compacte** : Meilleure utilisation de l'espace écran
- Plus d'espace vertical pour l'affichage des webviews
- Tous les contrôles accessibles sur une seule ligne
- Tailles des éléments légèrement réduites (boutons 36x36px, textes 13-14px)
- Padding optimisé sur tous les éléments
### Technique
- Nouvelle classe CSS `.header-with-tabs` remplaçant l'ancien header séparé
- Onglets avec `flex: 1` pour occuper l'espace disponible
- Hauteur du conteneur principal ajustée à `calc(100vh - 60px)`
- Webview container à 100% de hauteur (plus de déduction pour les onglets)
## [1.2.0] - 2025-09-04
### Supprimé
- **Sidebar latérale gauche** : Interface simplifiée avec suppression complète du panneau latéral
- Plus de liste des centres dans la sidebar (navigation uniquement par onglets)
- Suppression des statistiques du jour (appels traités et RDV pris)
- Gain d'espace significatif pour l'affichage des webviews
- **Bouton "Simuler un appel"** : Fonctionnalité de simulation retirée
- Suppression du bouton dans le header
- Modal de simulation complètement retirée
- Code JavaScript associé nettoyé (fonctions showCallSimulation, loadSimulatedCalls, etc.)
- Référence au script cti-simulator.js supprimée
- **Scrollbars visibles** : Masquage complet des barres de défilement
- Scrollbar verticale supprimée via overflow: hidden
- Scrollbar horizontale des onglets masquée (reste fonctionnelle au scroll)
### Ajouté
- **Zone de notes dynamique** : Panneau de notes rapides affichable/masquable
- Nouveau bouton 📝 dans le header pour toggle les notes
- Animation fluide de glissement depuis le bas de l'écran
- Bouton × pour fermer rapidement le panneau
- Sauvegarde automatique des préférences dans localStorage
- Panneau masqué par défaut pour maximiser l'espace de travail
### Modifié
- **Interface modernisée** : Refonte complète du design
- Nouveau fichier styles-modern.css remplaçant l'ancien styles.css
- Header épuré avec ombres subtiles et animations
- Onglets style Material Design avec indicateur actif coloré
- Boutons avec effets hover et transitions fluides
- Palette de couleurs plus moderne et contrastée
- Animations et transitions ajoutées partout
- **Optimisation de l'espace** : Meilleure utilisation de l'écran
- Les webviews occupent maintenant toute la largeur disponible
- Hauteurs calculées précisément avec calc() CSS
- Interface responsive et adaptative
- **Structure HTML simplifiée** : Code plus propre et maintenable
- Suppression des éléments DOM liés à la sidebar
- Suppression de la modal de simulation
- Organisation plus claire des sections
### Technique
- Gestion des préférences utilisateur via localStorage
- Fonctions JavaScript ajoutées : toggleNotes(), showNotes(), hideNotes(), loadUserPreferences()
- CSS moderne avec animations @keyframes et transitions
- Masquage des scrollbars compatible tous navigateurs (webkit, Firefox, IE/Edge)
- Hauteurs calculées dynamiquement pour éviter les débordements
## [1.1.3] - 2025-09-04
### Modifié
- **Onglets des plannings** : Affichage du code client au lieu du nom de la file d'attente
- Les titres des onglets affichent maintenant `centre.id` (code client) au lieu de `centre.nom` (queueName)
- Permet une identification plus directe et claire du client concerné
- Modification dans renderer.js ligne 227
## [1.1.2] - 2025-09-04
### Modifié
- **Checkbox de déconnexion** : Renommage du libellé "Déconnexion" en "Débloquer"
- Terme plus clair et explicite pour l'utilisateur
- Meilleure compréhension de l'action (débloquer une session)
- **Titre de l'application** : Simplification en "SimpleConnect"
- Suppression du sous-titre "Gestion Centralisée des Plannings"
- Titre plus concis dans la barre de titre et l'onglet du navigateur
## [1.1.1] - 2025-09-04
### Ajouté
- **Option de déconnexion forcée** sur l'écran de connexion
- Nouvelle checkbox permettant de forcer la fermeture d'une session précédente
- Utile en cas de déconnexion anormale, crash ou changement de poste
- Appel à `AgentLogoff` avant la nouvelle connexion si l'option est cochée
- Message adaptatif sur le bouton ("Reconnexion..." au lieu de "Connexion en cours...")
### Modifié
- **Interface de connexion** : Ajout d'un conteneur stylisé pour l'option de déconnexion
- Design moderne avec checkbox personnalisée
- Fond gris clair avec bordures arrondies
- Texte d'aide explicatif sous l'option
- Effet hover sur le conteneur pour améliorer l'UX
- **main.js** : Logique de déconnexion forcée dans le handler `login-agent`
- Vérification du paramètre `forceDisconnect` dans les credentials
- Tentative de déconnexion avec gestion d'erreur silencieuse
- Continuation du processus même si la déconnexion échoue
- **renderer.js** : Récupération et transmission de l'état de la checkbox
- Lecture de l'état `forceDisconnect` depuis le formulaire
- Ajout du paramètre dans l'objet credentials
- Adaptation du texte du bouton selon l'option choisie
### Style
- **CSS personnalisé** pour la checkbox de déconnexion forcée
- Checkbox native HTML avec design moderne
- Indicateur visuel coché/non coché avec transitions fluides
- Alignement parfait avec le texte "Déconnexion"
- Responsive et accessible
### Technique
- Implémentation non-bloquante : la connexion continue même si `AgentLogoff` échoue
- Gestion des sessions fantômes après crash ou perte réseau
- Message informatif dans les logs pour tracer les déconnexions forcées
## [1.1.0] - 2025-09-04
### Ajouté
- **Authentification SignalR réelle** : Remplacement de l'authentification locale simulée par l'authentification SignalR
- Connexion via `AgentLogin` avec email, password et terminal
- Déconnexion propre via `AgentLogoff`
- Gestion automatique des sessions agents côté serveur
- **Traitement dynamique des centres** : Génération automatique depuis les données SignalR
- Remplacement des placeholders (#CA#, #MP#) dans les URLs
- Attribution automatique de couleurs aux centres
- Mapping avec les files téléphoniques pour le routage
- **Gestion des événements IPBX en temps réel**
- Basculement automatique vers le bon centre lors d'un appel entrant
- Libération automatique de la file après raccrochage
- Filtrage par terminal pour ne recevoir que les événements pertinents
- **Notifications visuelles** avec animations CSS
- Notifications temporaires pour les événements d'appels
- Animations slideIn/slideOut pour une meilleure UX
- Son de notification pour les appels entrants
### Modifié
- **main.js** : Refonte complète de l'authentification
- Suppression de la vérification locale dans config.json
- Ajout des handlers SignalR pour AgentLogin et AgentLogoff
- Implémentation des événements IpbxEvent (codes 1 et 2)
- Mise à jour dynamique du titre avec nom agent et terminal
- **renderer.js** : Adaptation pour SignalR
- Connexion directe via SignalR au lieu de config.json
- Écoute des événements switch-to-center et release-center
- Ajout des fonctions updateAgentStatus, showNotification, updateCallStats
- **Déconnexion améliorée** : Nettoyage propre des sessions
- Appel à AgentLogoff avant fermeture de l'application
- Réinitialisation complète des variables d'état
### Technique
- Stockage global de `agentConnectionInfo` pour les données SignalR
- Traitement des URLs avec fonction `processApplicationUrls()`
- Gestion des états de connexion SignalR avec reconnexion automatique
- Validation côté client du terminal avant envoi au serveur
### Supprimé
- Authentification locale basée sur config.json
- Dépendance aux données statiques pour les centres
- Configuration manuelle des centres dans config.json
## [1.0.2] - 2025-09-04
### Corrigé
- **DevTools** : Ouverture uniquement en mode développement au lieu de systématiquement
- **Mode développement** : Correction de la détection du mode dev pour le simulateur CTI via IPC
- **Choices.js** : Suppression de la boucle de retry infinie en cas d'échec de chargement
- **Code mort** : Suppression de la fonction `updateSignalRStatus()` vide et inutilisée
### Technique
- Ajout du handler IPC `is-development` pour permettre au renderer de détecter le mode
- Amélioration de la gestion d'erreur pour Choices.js avec fallback natif sans retry
- Nettoyage du code en supprimant les fonctions vides et appels inutiles
## [1.0.1] - 2024-09-04
### Ajouté
- **Intégration de Choices.js** pour améliorer l'expérience utilisateur sur le champ de sélection des postes téléphoniques
- Liste déroulante moderne avec recherche instantanée
- Support de plus de 100 postes téléphoniques
- Interface utilisateur améliorée avec scrollbar personnalisée
- Recherche en temps réel pour trouver rapidement un poste
### Modifié
- **Page de connexion** : Remplacement du select HTML natif par Choices.js
- Amélioration visuelle avec thème personnalisé (violet #667eea)
- Ajout d'animations fluides à l'ouverture/fermeture
- Indicateurs visuels pour les éléments sélectionnés
- **Chargement des ressources** : Migration des CDN vers des fichiers locaux pour éviter les problèmes CSP
- **Gestion des erreurs** : Amélioration du fallback en cas d'échec de chargement de Choices.js
### Technique
- Installation locale de Choices.js via npm
- Copie des fichiers CSS/JS dans le répertoire racine
- Adaptation du code pour gérer window.Choices dans le contexte Electron
- Suppression du groupement par centaine pour simplifier la navigation
### Corrigé
- Résolution des erreurs Content Security Policy (CSP) avec les ressources CDN
- Correction de l'initialisation de Choices.js dans l'environnement Electron
- Fix des classes CSS pour éviter les erreurs DOMTokenList
## [1.0.0] - 2024-09-01
### Fonctionnalités initiales
- **Connexion agent** avec authentification locale
- **Intégration SignalR** pour la communication avec le serveur CTI
- **Récupération dynamique** des postes téléphoniques depuis le serveur
- **Multi-centres** : Gestion de plusieurs centres médicaux
- **Webviews intégrées** pour afficher les plannings médicaux
- **Simulation d'appels** pour les tests et démos
- **Interface moderne** avec design violet/blanc
- **Statistiques journalières** : Compteurs d'appels et de RDV
- **Notes rapides** : Prise de notes pendant les appels
- **Indicateurs visuels** : États de connexion SignalR et disponibilité agent

View File

@@ -13,9 +13,9 @@
<div class="login-container"> <div class="login-container">
<h1>SimpleConnect</h1> <h1>SimpleConnect</h1>
<div class="app-version-login" id="appVersionLogin"></div> <div class="app-version-login" id="appVersionLogin"></div>
<div class="signalr-status"> <div class="server-status">
<span class="signalr-indicator" id="signalrIndicator"></span> <span class="server-indicator" id="serverIndicator"></span>
<span class="signalr-text" id="signalrText" <span class="server-text" id="serverText"
>Connexion au serveur...</span >Connexion au serveur...</span
> >
</div> </div>
@@ -34,23 +34,11 @@
placeholder="Mot de passe" placeholder="Mot de passe"
required required
/> />
<select id="terminal" required> <select id="terminal" required disabled>
<option value="" placeholder>Chargement des postes...</option> <option value="">En attente du serveur...</option>
</select> </select>
<!-- Option de déconnexion forcée --> <button type="submit" disabled>Se connecter</button>
<div class="force-disconnect-container">
<label class="checkbox-label">
<input type="checkbox" id="forceDisconnect" />
<span>Débloquer</span>
</label>
<small class="checkbox-hint"
>À cocher en cas de problème de connexion ou de session
bloquée.</small
>
</div>
<button type="submit">Se connecter</button>
<button type="button" id="quitLoginBtn" class="btn-quit">Quitter</button> <button type="button" id="quitLoginBtn" class="btn-quit">Quitter</button>
<div id="loginError" class="error-message"></div> <div id="loginError" class="error-message"></div>
</form> </form>

662
main.js
View File

@@ -1,51 +1,80 @@
const { app, BrowserWindow, ipcMain, session } = require('electron'); const { app, BrowserWindow, ipcMain, session, net } = require('electron');
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const os = require('os'); const os = require('os');
const signalR = require('@microsoft/signalr'); const SocketIOAdapter = require('./socketio-adapter');
const ConnectionManager = require('./connection-manager');
let mainWindow; let mainWindow;
let config; let config;
let currentAgent = null; let currentAgent = null;
let currentTerminal = null; // Terminal téléphonique de l'agent connecté let currentTerminal = null;
let signalRConnection = null; let adapter = null;
let signalRStatus = 'disconnected'; // disconnected, connecting, connected, error let serverStatus = 'disconnected'; // disconnected, connecting, connected, error
let agentConnectionInfo = null; // Informations complètes retournées par SignalR let agentConnectionInfo = null;
let healthCheckInterval = null;
// Configuration du système de logs SignalR // Configuration du systeme de logs
const SIGNALR_LOG_DIR = path.join(os.homedir(), '.simpleconnect-ng'); const LOG_DIR = path.join(os.homedir(), '.simpleconnect-ng');
const SIGNALR_LOG_FILE = path.join(SIGNALR_LOG_DIR, 'signalr.log'); const LOG_FILE = path.join(LOG_DIR, 'socketio.log');
// Créer le répertoire de logs s'il n'existe pas
function ensureLogDirectory() { function ensureLogDirectory() {
if (!fs.existsSync(SIGNALR_LOG_DIR)) { if (!fs.existsSync(LOG_DIR)) {
fs.mkdirSync(SIGNALR_LOG_DIR, { recursive: true }); fs.mkdirSync(LOG_DIR, { recursive: true });
console.log(`📁 Répertoire de logs créé: ${SIGNALR_LOG_DIR}`);
} }
} }
// Fonction pour écrire dans le fichier de log SignalR function stripAnsi(str) {
function logToSignalRFile(message, data = null) { return str.replace(/\x1b\[[0-9;]*m/g, '');
}
function fileTimestamp() {
return new Date().toLocaleString('fr-FR', {
timeZone: 'Europe/Paris',
day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit', second: '2-digit'
});
}
function logToFile(message, data = null) {
ensureLogDirectory(); ensureLogDirectory();
let line = `[${fileTimestamp()}] ${stripAnsi(message)}`;
const timestamp = new Date().toISOString(); if (data) line += ' ' + JSON.stringify(data);
let logEntry = `[${timestamp}] ${message}`; fs.appendFileSync(LOG_FILE, line + '\n', 'utf8');
if (data) {
logEntry += '\n' + JSON.stringify(data, null, 2);
}
logEntry += '\n' + '─'.repeat(80) + '\n';
// Ajouter au fichier (append)
fs.appendFileSync(SIGNALR_LOG_FILE, logEntry, 'utf8');
} }
// Logger dans la console ET dans le fichier function logSectionToFile(message) {
function logSignalR(message, data = null) { ensureLogDirectory();
console.log(message, data || ''); fs.appendFileSync(LOG_FILE, `[${fileTimestamp()}] ── ${stripAnsi(message)} ──\n`, 'utf8');
logToSignalRFile(message, data); }
// ANSI colors
const c = {
reset: '\x1b[0m',
dim: '\x1b[2m',
green: '\x1b[32m',
red: '\x1b[31m',
cyan: '\x1b[36m',
bold: '\x1b[1m',
};
function formatTimestamp() {
return new Date().toLocaleString('fr-FR', {
timeZone: 'Europe/Paris',
hour: '2-digit', minute: '2-digit', second: '2-digit'
});
}
function log(message, data = null) {
console.log(`${c.dim}${formatTimestamp()}${c.reset} ${message}`);
logToFile(message, data);
}
function logBanner(version, serverUrl) {
console.log('');
console.log(` ${c.bold}${c.cyan}SimpleConnect${c.reset} ${c.dim}v${version}${c.reset}`);
console.log(` ${c.dim}Serveur${c.reset} ${serverUrl}`);
console.log('');
logSectionToFile(`SimpleConnect v${version}${serverUrl}`);
} }
// Charger la configuration // Charger la configuration
@@ -55,7 +84,7 @@ function loadConfig() {
config = JSON.parse(configData); config = JSON.parse(configData);
} }
// Créer la fenêtre principale // Creer la fenetre principale
function createWindow() { function createWindow() {
mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
width: 1400, width: 1400,
@@ -68,211 +97,120 @@ function createWindow() {
}, },
icon: path.join(__dirname, 'icon.png'), icon: path.join(__dirname, 'icon.png'),
title: `SimpleConnect v${app.getVersion()}`, title: `SimpleConnect v${app.getVersion()}`,
autoHideMenuBar: true // Cache la barre de menu par défaut autoHideMenuBar: true
}); });
// Supprimer complètement le menu (pour Linux/Windows)
mainWindow.setMenuBarVisibility(false); mainWindow.setMenuBarVisibility(false);
// Charger l'interface HTML
mainWindow.loadFile('index.html'); mainWindow.loadFile('index.html');
// Forcer le titre après le chargement de la page (le <title> HTML l'écrase sinon)
mainWindow.webContents.on('did-finish-load', () => { mainWindow.webContents.on('did-finish-load', () => {
mainWindow.setTitle(`SimpleConnect v${app.getVersion()}`); mainWindow.setTitle(`SimpleConnect v${app.getVersion()}`);
}); });
// Ouvrir les DevTools uniquement en mode développement
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
mainWindow.webContents.openDevTools(); mainWindow.webContents.openDevTools();
} }
// Gérer la fermeture de la fenêtre
mainWindow.on('closed', () => { mainWindow.on('closed', () => {
mainWindow = null; mainWindow = null;
}); });
} }
// === GESTION SIGNALR/WEBSOCKET === // === GESTION SOCKET.IO ===
function initializeSignalR() {
if (!config.signalR || !config.signalR.enabled) { function initializeSocketIO() {
console.log('SignalR/WebSocket désactivé dans la configuration'); if (!config.socketio || !config.socketio.serverUrl) {
signalRStatus = 'disabled'; log('Socket.IO non configuré');
sendSignalRStatus(); serverStatus = 'disabled';
sendServerStatus();
return; return;
} }
try { logBanner(app.getVersion(), config.socketio.serverUrl);
// Utiliser le ConnectionManager avec fallback automatique SignalR → WebSocket
const connectionManager = new ConnectionManager(config.ServerIp || config.signalR.serverUrl.replace('http://', '').replace('/signalR', ''));
// La connexion sera établie plus tard avec fallback automatique adapter = new SocketIOAdapter(config.socketio.serverUrl);
signalRConnection = connectionManager;
// Les handlers d'état seront configurés après la connexion // Demarrer le health check (ecran de login)
startHealthCheck();
// Démarrer la connexion (les handlers seront configurés après)
startSignalRConnection();
} catch (error) {
console.error('Erreur initialisation SignalR:', error);
signalRStatus = 'error';
sendSignalRStatus();
}
} }
function setupSignalRHandlers() { // Health check periodique via GET /health
// Configuration des handlers après la connexion // Actif uniquement sur l'ecran de login (pas d'agent connecte)
const connection = signalRConnection.getConnection(); function startHealthCheck() {
if (!connection) { const checkHealth = () => {
console.error('Pas de connexion active pour configurer les handlers'); // Ne pas faire de health check quand un agent est connecte
return; // (c'est l'adapter Socket.IO qui pilote le voyant)
} if (currentAgent) return;
// === LOGGER UNIVERSEL POUR TOUS LES MESSAGES SIGNALR === const serverUrl = config.socketio.serverUrl;
// Intercepter TOUS les messages reçus du serveur pour découvrir les événements disponibles let done = false;
// Initialiser le fichier de log avec une session const request = net.request(`${serverUrl}/health`);
logSignalR('════════════════════════════════════════════════════════════');
logSignalR('🚀 NOUVELLE SESSION SIGNALR DÉMARRÉE');
logSignalR(`Application: SimpleConnect v${app.getVersion()}`);
logSignalR(`Serveur SignalR: ${config.signalR.serverUrl}`);
logSignalR(`Service Provider: ${config.signalR.serviceProvider}`);
logSignalR('════════════════════════════════════════════════════════════');
// Liste des événements connus pour les logger différemment request.on('response', (response) => {
const knownEvents = ['IpbxEvent']; if (done) return;
done = true;
// Créer un proxy pour intercepter tous les appels .on() const newStatus = response.statusCode === 200 ? 'connected' : 'error';
const originalOn = signalRConnection.on.bind(signalRConnection); if (serverStatus !== newStatus) {
serverStatus = newStatus;
// Logger tous les événements possibles en essayant d'écouter les plus communs sendServerStatus();
const possibleEvents = [
'IpbxEvent', // Événements téléphoniques (confirmé)
'AgentStatusChanged', // Changement de statut agent
'QueueUpdate', // Mise à jour des files
'CallReceived', // Appel entrant
'CallEnded', // Fin d'appel
'MessageReceived', // Messages
'Notification', // Notifications générales
'StatusUpdate', // Mises à jour de statut
'SystemMessage', // Messages système
'BroadcastMessage', // Messages broadcast
'AgentUpdate', // Mises à jour agent
'QueueStatistics', // Statistiques de file
'PresenceUpdate' // Mise à jour de présence
];
// Écouter tous les événements possibles et logger ce qu'on reçoit
possibleEvents.forEach(eventName => {
connection.on(eventName, (...args) => {
// Logger dans la console avec formatage
console.log('═══════════════════════════════════════════════════════════');
console.log(`📨 MESSAGE SIGNALR REÇU: ${eventName}`);
console.log('Timestamp:', new Date().toISOString());
console.log('Nombre d\'arguments:', args.length);
// Logger chaque argument en détail
args.forEach((arg, index) => {
console.log(`Argument ${index}:`, JSON.stringify(arg, null, 2));
});
console.log('═══════════════════════════════════════════════════════════');
// Logger dans le fichier avec structure
const logData = {
event: eventName,
timestamp: new Date().toISOString(),
argumentCount: args.length,
arguments: args.map((arg, index) => ({
index: index,
type: typeof arg,
value: arg
})),
agent: currentAgent ? {
id: currentAgent.id,
name: currentAgent.name,
terminal: currentTerminal
} : null
};
logSignalR(`📨 MESSAGE SIGNALR REÇU: ${eventName}`, logData);
// Si c'est IpbxEvent, traiter comme avant
if (eventName === 'IpbxEvent') {
handleIpbxEventOriginal(args);
} }
}); });
});
// Fonction originale pour traiter les IpbxEvent request.on('error', () => {
function handleIpbxEventOriginal(args) { if (done) return;
if (!args || !agentConnectionInfo) return; done = true;
if (serverStatus !== 'error') {
const [name, eventArgs] = args; serverStatus = 'error';
const event = eventArgs?.[0] || args[0]; sendServerStatus();
}
console.log('🔍 Traitement IpbxEvent:', {
eventCode: event.eventCode,
terminal: event.terminal,
queueName: event.queueName,
fullEvent: event // Logger l'objet complet pour voir tous les champs
}); });
// Vérifier que l'événement est pour notre terminal // Timeout 5s — net.request n'a pas de timeout natif
if (event.terminal !== currentTerminal) { setTimeout(() => {
console.log('⚠️ Événement ignoré - Terminal différent:', event.terminal, '!==', currentTerminal); if (!done) {
return; done = true;
} request.abort();
if (serverStatus !== 'error') {
serverStatus = 'error';
sendServerStatus();
}
}
}, 5000);
// Gérer les différents types d'événements request.end();
switch(event.eventCode) { };
case 0:
console.log('📞 Code 0: Appel entrant/sonnerie (non implémenté)');
break;
case 1: // Appel décroché
console.log('✅ Code 1: Appel décroché - Traitement...');
handleCallPickedUp(event);
break;
case 2: // Appel raccroché
console.log('📴 Code 2: Appel raccroché - Traitement...');
handleCallHungUp(event);
break;
case 3:
console.log('⏸️ Code 3: Mise en attente (non implémenté)');
break;
case 4:
console.log('↔️ Code 4: Transfert d\'appel (non implémenté)');
break;
case 5:
console.log('👥 Code 5: Conférence (non implémenté)');
break;
default:
console.log('❓ Code événement inconnu:', event.eventCode);
console.log('Données complètes:', JSON.stringify(event, null, 2));
}
}
// Logger aussi les méthodes qu'on peut invoquer // Check immediat puis toutes les 5s
console.log('📋 MÉTHODES SIGNALR DISPONIBLES POUR INVOCATION:'); checkHealth();
console.log('- AgentLogin(email, password, terminal)'); healthCheckInterval = setInterval(checkHealth, 5000);
console.log('- AgentLogoff(accessCode)');
console.log('- GetTerminalListByServiceProvider(serviceProvider)');
console.log(' D\'autres méthodes peuvent être disponibles sur le serveur');
} }
// Gérer un appel entrant function stopHealthCheck() {
if (healthCheckInterval) {
clearInterval(healthCheckInterval);
healthCheckInterval = null;
}
}
function sendServerStatus() {
if (serverStatus !== 'connected') {
log(`${c.red}${c.reset} Serveur injoignable`);
}
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('server-status', serverStatus);
}
}
// Gerer un appel entrant
function handleCallPickedUp(event) { function handleCallPickedUp(event) {
if (!mainWindow || !agentConnectionInfo) return; if (!mainWindow || !agentConnectionInfo) return;
// Identifier le centre correspondant à la file
const centres = processApplicationUrls(agentConnectionInfo.connList); const centres = processApplicationUrls(agentConnectionInfo.connList);
const centre = centres.find(c => c.queueName === event.queueName); const centre = centres.find(c => c.queueName === event.queueName);
if (centre) { if (centre) {
console.log('Basculement vers le centre:', centre.nom); log(`Basculement vers le centre: ${centre.nom}`);
// Envoyer l'instruction de basculement à la fenêtre
mainWindow.webContents.send('switch-to-center', { mainWindow.webContents.send('switch-to-center', {
centreId: centre.id, centreId: centre.id,
centreName: centre.nom, centreName: centre.nom,
@@ -281,17 +219,15 @@ function handleCallPickedUp(event) {
eventType: 'call_pickup' eventType: 'call_pickup'
}); });
} else { } else {
console.warn('Aucun centre trouvé pour la file:', event.queueName); log(`${c.yellow}Aucun centre trouvé pour la file: ${event.queueName}${c.reset}`);
} }
} }
// Gérer la fin d'un appel // Gerer la fin d'un appel
function handleCallHungUp(event) { function handleCallHungUp(event) {
if (!mainWindow) return; if (!mainWindow) return;
console.log('Fin d\'appel sur la file:', event.queueName); log(`Fin d'appel sur la file: ${event.queueName}`);
// Envoyer l'instruction de libération à la fenêtre
mainWindow.webContents.send('release-center', { mainWindow.webContents.send('release-center', {
queueName: event.queueName, queueName: event.queueName,
terminal: event.terminal, terminal: event.terminal,
@@ -299,68 +235,45 @@ function handleCallHungUp(event) {
}); });
} }
async function startSignalRConnection() { // Configurer les handlers d'evenements Socket.IO apres connexion
try { function setupEventHandlers() {
signalRStatus = 'connecting'; if (!adapter) return;
sendSignalRStatus();
logSignalR('🔌 Tentative de connexion au serveur...', {
serverUrl: config.ServerIp || config.signalR.serverUrl,
status: 'connecting'
});
// Le ConnectionManager gère le fallback automatiquement log('Session Socket.IO démarrée', {
const connection = await signalRConnection.connect(); serverUrl: config.socketio.serverUrl,
serviceProvider: config.socketio.serviceProvider
});
// Déterminer quel type de connexion a réussi // Ecouter les evenements d'appels IPBX
const connectionInfo = signalRConnection.getConnectionInfo(); adapter.on('ipbx_event', (data) => {
log('ipbx_event recu', data);
console.log(`Connexion établie via ${connectionInfo.type}`); if (!agentConnectionInfo) return;
logSignalR(`✅ Connexion établie avec succès (${connectionInfo.type})`, {
connectionType: connectionInfo.type,
isSignalR: connectionInfo.isSignalR,
isRestSocketIO: connectionInfo.isRestSocketIO,
status: 'connected',
serverUrl: connectionInfo.serverUrl
});
// Maintenant configurer les handlers sur la connexion active // Verifier que l'evenement est pour notre terminal
setupSignalRHandlers(); if (data.terminal !== currentTerminal) {
log(`Événement ignoré — terminal différent: ${data.terminal} !== ${currentTerminal}`);
return;
}
signalRStatus = 'connected'; switch (data.eventCode) {
sendSignalRStatus(); case 1:
handleCallPickedUp(data);
} catch (error) { break;
console.error('Erreur connexion SignalR:', error); case 2:
logSignalR('❌ Erreur de connexion SignalR', { handleCallHungUp(data);
error: error.message, break;
stack: error.stack, default:
serverUrl: config.signalR.serverUrl log('Code evenement non gere:', data);
}); }
signalRStatus = 'error'; });
sendSignalRStatus();
// Réessayer dans 5 secondes
setTimeout(() => {
if (signalRConnection && signalRStatus !== 'connected') {
startSignalRConnection();
}
}, 5000);
}
}
function sendSignalRStatus() {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('signalr-status', signalRStatus);
}
} }
// Initialisation de l'application // Initialisation de l'application
app.whenReady().then(() => { app.whenReady().then(() => {
// Supprimer le menu de l'application complètement (toutes plateformes)
const { Menu } = require('electron'); const { Menu } = require('electron');
Menu.setApplicationMenu(null); Menu.setApplicationMenu(null);
// Configuration de la session pour éviter les problèmes CORS
session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => { session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
details.requestHeaders['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'; details.requestHeaders['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
callback({ requestHeaders: details.requestHeaders }); callback({ requestHeaders: details.requestHeaders });
@@ -377,33 +290,31 @@ app.whenReady().then(() => {
loadConfig(); loadConfig();
createWindow(); createWindow();
initializeSocketIO();
// Initialiser SignalR après le chargement de la config
initializeSignalR();
}); });
// Quitter quand toutes les fenêtres sont fermées // Quitter quand toutes les fenetres sont fermees
app.on('window-all-closed', async () => { app.on('window-all-closed', async () => {
if (healthCheckInterval) {
clearInterval(healthCheckInterval);
}
// Déconnexion propre avant de quitter // Déconnexion propre avant de quitter
if (currentAgent && signalRConnection && signalRStatus === 'connected') { if (currentAgent && adapter) {
try { try {
await signalRConnection.invoke('AgentLogoff', currentAgent.accessCode); await adapter.logoff();
console.log('Déconnexion agent avant fermeture');
} catch (error) { } catch (error) {
console.error('Erreur déconnexion:', error); log(`${c.red}Erreur déconnexion: ${error.message}${c.reset}`);
} }
} }
// Ne pas appeler disconnect() pour éviter l'envoi du CloseMessage
// Le serveur .NET ne supporte pas ce message
// On laisse la connexion se fermer naturellement avec l'application
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
log(`${c.dim}Application fermée${c.reset}`);
logSectionToFile('Application fermée');
app.quit(); app.quit();
} }
}); });
// Réactiver l'app sur macOS
app.on('activate', () => { app.on('activate', () => {
if (mainWindow === null) { if (mainWindow === null) {
createWindow(); createWindow();
@@ -412,108 +323,119 @@ app.on('activate', () => {
// === IPC HANDLERS === // === IPC HANDLERS ===
// Obtenir la configuration
ipcMain.handle('get-config', () => { ipcMain.handle('get-config', () => {
return config; return config;
}); });
// Obtenir la version de l'application
ipcMain.handle('get-app-version', () => { ipcMain.handle('get-app-version', () => {
return app.getVersion(); return app.getVersion();
}); });
// Obtenir le statut SignalR ipcMain.handle('get-server-status', () => {
ipcMain.handle('get-signalr-status', () => { return serverStatus;
return signalRStatus;
}); });
// Récupérer la liste des terminaux téléphoniques // Recuperer la liste des terminaux via REST
ipcMain.handle('get-terminal-list', async () => { ipcMain.handle('get-terminal-list', async () => {
// Mode simulation si SignalR non connecté if (!config.socketio || !config.socketio.serverUrl) {
if (!signalRConnection || signalRStatus !== 'connected') { return [];
console.log('SignalR non connecté, utilisation des terminaux de simulation');
return config.signalR.terminalsSimulation || ['3001', '3002', '3003'];
} }
try { try {
console.log('Récupération des terminaux pour:', config.signalR.serviceProvider); const provider = config.socketio.serviceProvider;
logSignalR('📡 Invocation SignalR: GetTerminalListByServiceProvider', { const url = `${config.socketio.serverUrl}/terminals?provider=${encodeURIComponent(provider)}`;
method: 'GetTerminalListByServiceProvider', return new Promise((resolve, reject) => {
serviceProvider: config.signalR.serviceProvider const request = net.request(url);
}); let body = '';
const terminals = await signalRConnection.invoke( request.on('response', (response) => {
'GetTerminalListByServiceProvider', response.on('data', (chunk) => {
config.signalR.serviceProvider body += chunk.toString();
); });
console.log('Terminaux disponibles:', terminals); response.on('end', () => {
logSignalR('📞 Terminaux récupérés', { if (response.statusCode === 200) {
count: terminals.length, try {
terminals: terminals const terminals = JSON.parse(body);
log(`${c.green}${c.reset} Serveur connecté — ${terminals.length} terminaux`);
resolve(Array.isArray(terminals) ? terminals : []);
} catch (e) {
log(`${c.red}Erreur parsing terminaux: ${e.message}${c.reset}`);
resolve([]);
}
} else {
log(`${c.red}Erreur terminaux HTTP ${response.statusCode}${c.reset}`);
resolve([]);
}
});
});
request.on('error', (error) => {
log(`${c.red}Erreur récupération terminaux: ${error.message}${c.reset}`);
resolve([]);
});
request.end();
}); });
return terminals || [];
} catch (error) { } catch (error) {
console.error('Erreur récupération terminaux:', error); log(`${c.red}Erreur récupération terminaux: ${error.message}${c.reset}`);
// Retourner les terminaux de simulation en cas d'erreur return [];
return config.signalR.terminalsSimulation || ['3001', '3002', '3003'];
} }
}); });
// Connexion agent via SignalR // Connexion agent via Socket.IO
ipcMain.handle('login-agent', async (event, credentials) => { ipcMain.handle('login-agent', async (event, credentials) => {
// Vérifier que SignalR est connecté if (!adapter) {
if (!signalRConnection || signalRStatus !== 'connected') {
return { return {
success: false, success: false,
message: 'Connexion au serveur SignalR non établie. Veuillez réessayer.' message: 'Socket.IO non initialise. Veuillez reessayer.'
}; };
} }
try { try {
console.log('Tentative de connexion agent:', credentials.email, 'Terminal:', credentials.terminal); log(`Tentative de connexion agent: ${credentials.email} Terminal: ${credentials.terminal}`);
logSignalR('🔐 Tentative de connexion agent', {
email: credentials.email,
terminal: credentials.terminal,
forceDisconnect: credentials.forceDisconnect || false
});
// Si déconnexion forcée demandée, déconnecter d'abord la session précédente // Deconnecter l'adapter precedent s'il y en a un
if (credentials.forceDisconnect) { if (adapter.state === 'connected') {
console.log('Déconnexion forcée demandée pour:', credentials.email); adapter.disconnect();
try {
// Tenter la déconnexion avec le code d'accès
await signalRConnection.invoke('AgentLogoff', credentials.email);
console.log('Session précédente déconnectée avec succès');
logSignalR('🔓 Session précédente déconnectée (forceDisconnect)', {
email: credentials.email
});
} catch (logoffError) {
console.warn('Erreur lors de la déconnexion forcée (session peut-être déjà fermée):', logoffError.message);
logSignalR('⚠️ Erreur déconnexion forcée', {
email: credentials.email,
error: logoffError.message
});
// Continuer même si la déconnexion échoue - la session est peut-être déjà fermée
}
} }
// Appel SignalR pour l'authentification // Recreer l'adapter pour une connexion fraiche
logSignalR('📡 Invocation SignalR: AgentLogin', { adapter = new SocketIOAdapter(config.socketio.serverUrl);
method: 'AgentLogin',
email: credentials.email, // L'adapter pilote le voyant une fois connecte
terminal: credentials.terminal adapter.onStateChange((state) => {
log('Etat Socket.IO change', { state });
switch (state) {
case 'reconnecting':
serverStatus = 'connecting';
sendServerStatus();
break;
case 'connected':
serverStatus = 'connected';
sendServerStatus();
break;
case 'error':
case 'disconnected':
serverStatus = 'error';
sendServerStatus();
break;
}
}); });
const result = await signalRConnection.invoke('AgentLogin', // Configurer les handlers d'evenements AVANT connect
setupEventHandlers();
// Connexion avec auth au handshake
const result = await adapter.connect(
credentials.email, credentials.email,
credentials.password, credentials.password,
credentials.terminal credentials.terminal
); );
if (result) { if (result) {
console.log('Connexion réussie:', result); log(`${c.green}${c.reset} Connexion réussie: ${result.accessCode} (${result.firstName} ${result.lastName})`);
logSignalR('Connexion agent réussie', { log('Connexion agent réussie', {
accessCode: result.accessCode, accessCode: result.accessCode,
firstName: result.firstName, firstName: result.firstName,
lastName: result.lastName, lastName: result.lastName,
@@ -522,11 +444,9 @@ ipcMain.handle('login-agent', async (event, credentials) => {
connList: result.connList connList: result.connList
}); });
// Stocker les informations de connexion
agentConnectionInfo = result; agentConnectionInfo = result;
currentTerminal = credentials.terminal; currentTerminal = credentials.terminal;
// Créer l'objet agent pour compatibilité
currentAgent = { currentAgent = {
id: result.accessCode, id: result.accessCode,
accessCode: result.accessCode, accessCode: result.accessCode,
@@ -537,16 +457,19 @@ ipcMain.handle('login-agent', async (event, credentials) => {
terminal: credentials.terminal terminal: credentials.terminal
}; };
// Traiter les URLs des applications et créer les centres
const centres = processApplicationUrls(result.connList); const centres = processApplicationUrls(result.connList);
// Mettre à jour le titre de la fenêtre
if (mainWindow) { if (mainWindow) {
mainWindow.setTitle( mainWindow.setTitle(
`SimpleConnect v${app.getVersion()} - Agent: ${currentAgent.accessCode} (${result.firstName} ${result.lastName}) - Tel: ${credentials.terminal}` `SimpleConnect v${app.getVersion()} - Agent: ${currentAgent.accessCode} (${result.firstName} ${result.lastName}) - Tel: ${credentials.terminal}`
); );
} }
// Agent connecte → l'adapter pilote le voyant, on arrete le health check
stopHealthCheck();
serverStatus = 'connected';
sendServerStatus();
return { return {
success: true, success: true,
agent: currentAgent, agent: currentAgent,
@@ -554,10 +477,10 @@ ipcMain.handle('login-agent', async (event, credentials) => {
}; };
} }
return { success: false, message: 'Échec de l\'authentification' }; return { success: false, message: 'Echec de l\'authentification' };
} catch (error) { } catch (error) {
console.error('Erreur lors de la connexion agent:', error); log(`${c.red}Erreur connexion agent: ${error.message}${c.reset}`);
return { return {
success: false, success: false,
message: error.message || 'Erreur de connexion au serveur' message: error.message || 'Erreur de connexion au serveur'
@@ -580,7 +503,7 @@ function processApplicationUrls(connList) {
url = url.replace('#MP#', conn.password || ''); url = url.replace('#MP#', conn.password || '');
} }
// Gérer les cas spécifiques des plateformes connues // Gerer les cas specifiques des plateformes connues
if (url === 'pro.mondocteur.fr' || url.includes('mondocteur.fr')) { if (url === 'pro.mondocteur.fr' || url.includes('mondocteur.fr')) {
if (!url.startsWith('http')) { if (!url.startsWith('http')) {
url = 'https://pro.mondocteur.fr/backoffice.do'; url = 'https://pro.mondocteur.fr/backoffice.do';
@@ -590,11 +513,9 @@ function processApplicationUrls(connList) {
url = 'https://pro.doctolib.fr/signin'; url = 'https://pro.doctolib.fr/signin';
} }
} else if (!url.startsWith('http')) { } else if (!url.startsWith('http')) {
// Ajouter https:// par défaut si pas de protocole
url = `https://${url}`; url = `https://${url}`;
} }
// Créer l'objet centre compatible avec l'interface
return { return {
id: conn.code || `centre${index + 1}`, id: conn.code || `centre${index + 1}`,
nom: conn.queueName || conn.code || `Centre ${index + 1}`, nom: conn.queueName || conn.code || `Centre ${index + 1}`,
@@ -604,43 +525,41 @@ function processApplicationUrls(connList) {
username: conn.accessCode, username: conn.accessCode,
password: conn.password password: conn.password
}, },
queueName: conn.queueName // Garder pour le mapping avec les événements IPBX queueName: conn.queueName
}; };
}); });
} }
// Fonction helper pour attribuer des couleurs aux centres
function getColorForIndex(index) { function getColorForIndex(index) {
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#DDA0DD', '#FFD93D', '#6BCB77']; const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#DDA0DD', '#FFD93D', '#6BCB77'];
return colors[index % colors.length]; return colors[index % colors.length];
} }
// Déconnexion agent via SignalR // Deconnexion agent via Socket.IO
ipcMain.handle('logout', async () => { ipcMain.handle('logout', async () => {
if (currentAgent && signalRConnection && signalRStatus === 'connected') { if (currentAgent && adapter) {
try { try {
// Appeler SignalR pour la déconnexion log('Logoff agent', {
logSignalR('📡 Invocation SignalR: AgentLogoff', {
method: 'AgentLogoff',
accessCode: currentAgent.accessCode accessCode: currentAgent.accessCode
}); });
await signalRConnection.invoke('AgentLogoff', currentAgent.accessCode); await adapter.logoff();
console.log('Agent déconnecté du serveur SignalR'); log('Agent déconnecté du serveur');
logSignalR('👋 Agent déconnecté avec succès', { log('Agent déconnecté avec succès', {
accessCode: currentAgent.accessCode, accessCode: currentAgent.accessCode,
name: currentAgent.name name: currentAgent.name
}); });
} catch (error) { } catch (error) {
console.error('Erreur lors de la déconnexion SignalR:', error); log(`${c.red}Erreur déconnexion: ${error.message}${c.reset}`);
} }
} }
// Réinitialiser les variables locales
currentAgent = null; currentAgent = null;
currentTerminal = null; currentTerminal = null;
agentConnectionInfo = null; agentConnectionInfo = null;
// Réinitialiser le titre de la fenêtre // Retour ecran login → relancer le health check
startHealthCheck();
if (mainWindow) { if (mainWindow) {
mainWindow.setTitle(`SimpleConnect v${app.getVersion()}`); mainWindow.setTitle(`SimpleConnect v${app.getVersion()}`);
} }
@@ -648,22 +567,15 @@ ipcMain.handle('logout', async () => {
return { success: true }; return { success: true };
}); });
// Handler pour quitter l'application proprement
ipcMain.handle('quit-app', async () => { ipcMain.handle('quit-app', async () => {
// Ne pas appeler disconnect() pour éviter l'envoi du CloseMessage log(`${c.dim}Application fermée${c.reset}`);
// Le serveur .NET ne supporte pas ce message logSectionToFile('Application fermée');
// On laisse la connexion se fermer naturellement avec l'application
// (comme le fait le client de prod)
// Quitter l'application
app.quit(); app.quit();
}); });
// Obtenir l'agent actuel
ipcMain.handle('get-current-agent', () => { ipcMain.handle('get-current-agent', () => {
if (!currentAgent || !agentConnectionInfo) return null; if (!currentAgent || !agentConnectionInfo) return null;
// Retourner les centres traités depuis SignalR
const centres = processApplicationUrls(agentConnectionInfo.connList); const centres = processApplicationUrls(agentConnectionInfo.connList);
return { return {
@@ -673,36 +585,16 @@ ipcMain.handle('get-current-agent', () => {
}; };
}); });
// Simuler un appel entrant // Sauvegarder les notes de l'agent
ipcMain.handle('simulate-call', (event, callData) => {
// Envoyer l'événement d'appel entrant à la fenêtre
mainWindow.webContents.send('incoming-call', callData);
return { success: true };
});
// Obtenir les données pour simuler des appels
ipcMain.handle('get-simulated-calls', () => {
return config.cti.appelSimules.map(appel => {
const centre = config.centres.find(c => c.id === appel.centreId);
return {
...appel,
centreNom: centre ? centre.nom : 'Centre inconnu'
};
});
});
// Sauvegarder les notes de l'agent (un seul fichier par agent)
ipcMain.handle('save-notes', (event, noteData) => { ipcMain.handle('save-notes', (event, noteData) => {
const notesDir = path.join(__dirname, 'notes'); const notesDir = path.join(__dirname, 'notes');
if (!fs.existsSync(notesDir)) { if (!fs.existsSync(notesDir)) {
fs.mkdirSync(notesDir); fs.mkdirSync(notesDir);
} }
// Un seul fichier par agent, mis à jour à chaque sauvegarde
const fileName = `notes_${currentAgent.id}.json`; const fileName = `notes_${currentAgent.id}.json`;
const filePath = path.join(notesDir, fileName); const filePath = path.join(notesDir, fileName);
// Lire l'historique existant si le fichier existe
let notesData = { let notesData = {
agent: currentAgent.id, agent: currentAgent.id,
agentName: currentAgent.name, agentName: currentAgent.name,
@@ -712,11 +604,9 @@ ipcMain.handle('save-notes', (event, noteData) => {
history: [] history: []
}; };
// Si le fichier existe, préserver l'historique
if (fs.existsSync(filePath)) { if (fs.existsSync(filePath)) {
try { try {
const existingData = JSON.parse(fs.readFileSync(filePath, 'utf8')); const existingData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
// Ajouter l'ancienne note à l'historique si elle a changé
if (existingData.currentNote && existingData.currentNote !== noteData.content) { if (existingData.currentNote && existingData.currentNote !== noteData.content) {
notesData.history = existingData.history || []; notesData.history = existingData.history || [];
notesData.history.unshift({ notesData.history.unshift({
@@ -724,11 +614,10 @@ ipcMain.handle('save-notes', (event, noteData) => {
date: existingData.lastModified, date: existingData.lastModified,
centre: existingData.centre centre: existingData.centre
}); });
// Limiter l'historique à 50 entrées
notesData.history = notesData.history.slice(0, 50); notesData.history = notesData.history.slice(0, 50);
} }
} catch (error) { } catch (error) {
console.error('Erreur lecture notes existantes:', error); log(`${c.red}Erreur lecture notes existantes: ${error.message}${c.reset}`);
} }
} }
@@ -737,7 +626,6 @@ ipcMain.handle('save-notes', (event, noteData) => {
return { success: true, file: fileName }; return { success: true, file: fileName };
}); });
// Récupérer les notes de l'agent
ipcMain.handle('get-notes', () => { ipcMain.handle('get-notes', () => {
if (!currentAgent) return null; if (!currentAgent) return null;
@@ -750,7 +638,7 @@ ipcMain.handle('get-notes', () => {
const data = fs.readFileSync(filePath, 'utf8'); const data = fs.readFileSync(filePath, 'utf8');
return JSON.parse(data); return JSON.parse(data);
} catch (error) { } catch (error) {
console.error('Erreur lecture notes:', error); log(`${c.red}Erreur lecture notes: ${error.message}${c.reset}`);
return null; return null;
} }
} }
@@ -758,7 +646,6 @@ ipcMain.handle('get-notes', () => {
return null; return null;
}); });
// Obtenir l'historique des appels
ipcMain.handle('get-call-history', () => { ipcMain.handle('get-call-history', () => {
const historyFile = path.join(__dirname, 'call_history.json'); const historyFile = path.join(__dirname, 'call_history.json');
if (fs.existsSync(historyFile)) { if (fs.existsSync(historyFile)) {
@@ -768,7 +655,6 @@ ipcMain.handle('get-call-history', () => {
return []; return [];
}); });
// Sauvegarder un appel dans l'historique
ipcMain.handle('save-call-history', (event, callData) => { ipcMain.handle('save-call-history', (event, callData) => {
const historyFile = path.join(__dirname, 'call_history.json'); const historyFile = path.join(__dirname, 'call_history.json');
let history = []; let history = [];
@@ -784,14 +670,8 @@ ipcMain.handle('save-call-history', (event, callData) => {
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
}); });
// Garder seulement les 100 derniers appels
history = history.slice(0, 100); history = history.slice(0, 100);
fs.writeFileSync(historyFile, JSON.stringify(history, null, 2)); fs.writeFileSync(historyFile, JSON.stringify(history, null, 2));
return { success: true }; return { success: true };
}); });
// Vérifier si on est en mode développement
ipcMain.handle('is-development', () => {
return process.env.NODE_ENV === 'development';
});

View File

@@ -1,6 +1,6 @@
{ {
"name": "simpleconnect-electron", "name": "simpleconnect-electron",
"version": "1.5.0", "version": "2.0.0",
"description": "Application de gestion centralisée des plannings médicaux pour centres d'appels", "description": "Application de gestion centralisée des plannings médicaux pour centres d'appels",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
@@ -13,7 +13,8 @@
"build:linux-x64": "electron-builder --linux --x64", "build:linux-x64": "electron-builder --linux --x64",
"build:linux-arm64": "electron-builder --linux --arm64", "build:linux-arm64": "electron-builder --linux --arm64",
"dist": "electron-builder", "dist": "electron-builder",
"postinstall": "electron-builder install-app-deps" "postinstall": "electron-builder install-app-deps",
"test": "bun test"
}, },
"keywords": [ "keywords": [
"electron", "electron",
@@ -85,7 +86,6 @@
"url": "https://github.com/simpleconnect/electron-app.git" "url": "https://github.com/simpleconnect/electron-app.git"
}, },
"dependencies": { "dependencies": {
"@microsoft/signalr": "^9.0.6",
"choices.js": "^11.1.0", "choices.js": "^11.1.0",
"socket.io-client": "^4.8.1" "socket.io-client": "^4.8.1"
} }

View File

@@ -1,34 +0,0 @@
# SimpleConnect v1.2.16
**Date de release : 5 septembre 2025**
## 🎯 Points clés de cette version
Version stable en production avec l'ancien système SignalR uniquement.
## ✨ Fonctionnalités principales
### Système de logging SignalR complet
- Capture et analyse de tous les événements téléphoniques
- Fichier de log centralisé dans `~/.simpleconnect-ng/signalr.log`
- 13 types d'événements SignalR écoutés et loggés
- Format JSON structuré avec timestamp et contexte agent
### Corrections importantes
- **Icônes manquantes sur Linux** : Remplacement des emojis par des SVG inline
- **Barre de menu Electron** : Suppression complète sur tous les OS
- **Build Linux** : Support multi-architectures (x64, arm64, AppImage, .deb, .rpm)
## 📦 Fichiers disponibles
- `SimpleConnect-1.2.16.AppImage` - Pour Linux (toutes distributions)
## ⚠️ Limitations
- **Pas de support SocketIO** : Fonctionne uniquement avec serveur .NET/SignalR
- **Pas de fallback** : Si SignalR échoue, pas de connexion alternative
## 🔧 Configuration requise
- Serveur : SimpleConnect .NET avec SignalR activé
- URL : `http://10.90.20.201:8002/signalR`
- Service Provider : RDVPREM
---
*Cette version est actuellement déployée en production*

View File

@@ -1,55 +0,0 @@
# SimpleConnect v1.3.0
**Date de release : 12 septembre 2025**
## 🚀 Évolution majeure : Support dual protocole
Cette version introduit la **compatibilité totale** avec les backends Python/SocketIO tout en conservant le support SignalR existant.
## ✨ Nouveautés principales
### Support dual SignalR/SocketIO avec fallback automatique
- **ConnectionManager** : Gestionnaire intelligent qui essaie d'abord SignalR
- **Fallback automatique** : Bascule sur SocketIO si SignalR échoue
- **WebSocketAdapter** : Émulation complète de l'API SignalR avec socket.io-client
- **Transparence totale** : Même code, même API, quel que soit le protocole
### Architecture refactorisée
- `connection-manager.js` : Stratégie de connexion avec fallback
- `websocket-adapter.js` : Adaptateur SocketIO → SignalR
- Abstraction complète : Le code principal ne sait pas quel protocole est utilisé
## 🔄 Flux de connexion
1. **Tentative SignalR** → Serveur .NET existant
2. **Si échec** → Bascule automatique sur SocketIO
3. **Logs détaillés** → Indication du type de connexion active
## 📦 Fichiers disponibles
- `SimpleConnect-1.3.0.AppImage` - Pour Linux (toutes distributions)
- `SimpleConnect-1.3.0-arm64.dmg` - Pour macOS Apple Silicon
- `SimpleConnect-1.3.0-arm64-mac.zip` - Archive macOS alternative
## ✅ Compatibilité
### Serveurs supportés
-**Backend .NET/SignalR** (actuel en production)
-**Backend Python/FastAPI/SocketIO** (nouveau)
-**Migration transparente** entre les deux
### Dépendances ajoutées
- `socket.io-client` v4.8.1 pour le support WebSocket
## 🔧 Configuration
Aucun changement de configuration nécessaire ! Le système détecte automatiquement :
- Port 8002 avec `/signalR` → Utilise SignalR
- Port 8002 sans `/signalR` → Utilise SocketIO
## 📈 Améliorations techniques
- Pattern Adapter pour unifier les APIs
- Gestion des promesses asynchrones
- Reconnexion automatique sur les deux protocoles
- Messages de statut indiquant le protocole actif
---
*Version actuellement en production - Compatible avec l'infrastructure existante et future*

View File

@@ -1,82 +0,0 @@
# SimpleConnect v1.5.0 - Support serveur Python (REST + Socket.IO)
**Date de release** : 24 novembre 2025
## Résumé
Cette version majeure ajoute le support du nouveau serveur Python en parallèle du serveur .NET existant. Le client peut maintenant fonctionner avec les deux backends grâce à un système de fallback automatique.
## Nouveautés principales
### Adaptateur REST + Socket.IO
- **RestSocketAdapter** : Nouvel adaptateur qui émule l'API SignalR pour le serveur Python
- **REST API** : Utilisé pour les actions (login, logout, liste des terminaux)
- **Socket.IO** : Utilisé pour les événements temps réel (IpbxEvent)
- **Compatibilité** : Interface identique à SignalR, transparent pour le reste du code
### Système de fallback automatique
- **Priorité SignalR** : Le client tente d'abord de se connecter au serveur .NET (SignalR)
- **Fallback Python** : Si SignalR échoue, bascule automatiquement vers REST + Socket.IO
- **Transition transparente** : Aucune modification nécessaire côté utilisateur
## Améliorations techniques
- Nouvelle dépendance `socket.io-client` (v4.8.1)
- `ConnectionManager` mis à jour pour gérer les deux types de connexion
- Health check du serveur Python avant connexion Socket.IO
- Gestion des erreurs et reconnexion automatique
## Fichiers modifiés
- `rest-socket-adapter.js` (nouveau) : Adaptateur REST + Socket.IO
- `connection-manager.js` : Logique de fallback SignalR -> REST
- `main.js` : Import du nouvel adaptateur
## Compatibilité
- **Serveur .NET** : Fonctionne avec le serveur WCF/SignalR existant
- **Serveur Python** : Fonctionne avec le nouveau serveur FastAPI + Socket.IO
- **macOS** : 10.12+ (Sierra et versions ultérieures)
- **Linux** : Distributions x64 supportant AppImage
- **Electron** : 28.0.0
## Installation
### macOS
1. Télécharger `SimpleConnect-1.5.0-arm64.dmg`
2. Ouvrir le fichier DMG
3. Glisser SimpleConnect dans le dossier Applications
4. Lancer depuis Applications
### Linux
1. Télécharger `SimpleConnect-1.5.0.AppImage`
2. Rendre le fichier exécutable : `chmod +x SimpleConnect-1.5.0.AppImage`
3. Double-cliquer ou exécuter : `./SimpleConnect-1.5.0.AppImage`
## Configuration
Le client détecte automatiquement le type de serveur. Aucune configuration supplémentaire n'est requise.
Pour forcer l'utilisation du serveur Python, désactiver SignalR dans `config.json` :
```json
{
"signalR": {
"enabled": false
}
}
```
## Problèmes connus
Aucun problème connu pour cette version.
## Support
Pour toute question ou problème, contactez l'équipe SimpleConnect.
---
**Version précédente** : [v1.4.1](v1.4.1.md)

View File

@@ -29,21 +29,22 @@ document.addEventListener('DOMContentLoaded', async () => {
versionLoginElement.textContent = `v${appVersion}`; versionLoginElement.textContent = `v${appVersion}`;
} }
// Initialiser l'indicateur SignalR // Initialiser l'indicateur de statut serveur
// Écouter les changements de statut SignalR // Ecouter les changements de statut serveur
ipcRenderer.on('signalr-status', (event, status) => { let previousServerStatus = null;
updateSignalRIndicator(status); ipcRenderer.on('server-status', (event, status) => {
// Recharger les terminaux à chaque changement de statut updateServerIndicator(status);
loadTerminals(); // Recharger les terminaux uniquement quand la connexion (re)monte
if (status === 'connected' && previousServerStatus !== 'connected') {
loadTerminals();
}
previousServerStatus = status;
}); });
// Obtenir le statut initial SignalR // Obtenir le statut initial (pas de loadTerminals — on attend que le health check confirme 'connected')
const initialStatus = await ipcRenderer.invoke('get-signalr-status'); const initialStatus = await ipcRenderer.invoke('get-server-status');
updateSignalRIndicator(initialStatus); updateServerIndicator(initialStatus);
// Charger immédiatement les terminaux pour la page de login
await loadTerminals();
// Vérifier si un agent est déjà connecté // Vérifier si un agent est déjà connecté
const agentData = await ipcRenderer.invoke('get-current-agent'); const agentData = await ipcRenderer.invoke('get-current-agent');
@@ -129,7 +130,7 @@ document.addEventListener('DOMContentLoaded', async () => {
handleIncomingCall(callData); handleIncomingCall(callData);
}); });
// Écouter les événements SignalR de basculement de centre // Ecouter les evenements de basculement de centre
ipcRenderer.on('switch-to-center', (event, data) => { ipcRenderer.on('switch-to-center', (event, data) => {
console.log('Basculement vers le centre:', data.centreName); console.log('Basculement vers le centre:', data.centreName);
@@ -162,14 +163,13 @@ document.addEventListener('DOMContentLoaded', async () => {
}); });
}); });
// Connexion via SignalR // Connexion agent
async function handleLogin(e) { async function handleLogin(e) {
e.preventDefault(); e.preventDefault();
const accessCode = document.getElementById('accessCode').value; const accessCode = document.getElementById('accessCode').value;
const password = document.getElementById('password').value; const password = document.getElementById('password').value;
const terminal = document.getElementById('terminal').value; const terminal = document.getElementById('terminal').value;
const forceDisconnect = document.getElementById('forceDisconnect').checked;
const errorDiv = document.getElementById('loginError'); const errorDiv = document.getElementById('loginError');
const loginBtn = document.querySelector('#loginForm button[type="submit"]'); const loginBtn = document.querySelector('#loginForm button[type="submit"]');
@@ -210,26 +210,25 @@ async function handleLogin(e) {
localStorage.setItem('last-terminal', terminal); localStorage.setItem('last-terminal', terminal);
// Afficher la modal de progression de connexion // Afficher la modal de progression de connexion
showLoginProgress(forceDisconnect); showLoginProgress();
// Désactiver le bouton pendant la connexion // Désactiver le bouton pendant la connexion
loginBtn.disabled = true; loginBtn.disabled = true;
loginBtn.textContent = forceDisconnect ? 'Reconnexion...' : 'Connexion en cours...'; loginBtn.textContent = 'Connexion en cours...';
errorDiv.textContent = ''; errorDiv.textContent = '';
// Attendre un peu pour que l'animation soit visible // Attendre un peu pour que l'animation soit visible
await new Promise(resolve => setTimeout(resolve, 300)); await new Promise(resolve => setTimeout(resolve, 300));
try { try {
// Préparer les credentials pour SignalR // Preparer les credentials
const credentials = { const credentials = {
email: accessCode, // Utiliser directement le code agent comme email email: accessCode, // Utiliser directement le code agent comme email
password: password, password: password,
terminal: terminal, terminal: terminal,
forceDisconnect: forceDisconnect // Ajouter l'option de déconnexion forcée
}; };
// Appeler l'authentification SignalR // Appeler l'authentification
const result = await ipcRenderer.invoke('login-agent', credentials); const result = await ipcRenderer.invoke('login-agent', credentials);
if (result.success) { if (result.success) {
@@ -423,25 +422,17 @@ function resetLoginForm() {
const accessCode = document.getElementById('accessCode'); const accessCode = document.getElementById('accessCode');
const password = document.getElementById('password'); const password = document.getElementById('password');
const terminal = document.getElementById('terminal'); const terminal = document.getElementById('terminal');
const forceDisconnect = document.getElementById('forceDisconnect');
const loginError = document.getElementById('loginError'); const loginError = document.getElementById('loginError');
const loginBtn = document.querySelector('#loginForm button[type="submit"]'); const loginBtn = document.querySelector('#loginForm button[type="submit"]');
if (accessCode) accessCode.value = ''; if (accessCode) accessCode.value = '';
if (password) password.value = ''; if (password) password.value = '';
// Réinitialiser le terminal (garder la dernière sélection si elle existe)
// Ne pas réinitialiser le terminal pour garder la préférence
// Décocher la checkbox de déblocage
if (forceDisconnect) forceDisconnect.checked = false;
// Vider les messages d'erreur // Vider les messages d'erreur
if (loginError) loginError.textContent = ''; if (loginError) loginError.textContent = '';
// Réactiver le bouton et remettre le texte par défaut // Remettre le texte par défaut — l'état disabled depend du statut serveur
if (loginBtn) { if (loginBtn) {
loginBtn.disabled = false;
loginBtn.textContent = 'Se connecter'; loginBtn.textContent = 'Se connecter';
} }
} }
@@ -835,7 +826,7 @@ async function loadTerminals() {
console.log('Chargement des terminaux...'); console.log('Chargement des terminaux...');
try { try {
// Récupérer les terminaux depuis le serveur SignalR // Recuperer les terminaux depuis le serveur
const terminals = await ipcRenderer.invoke('get-terminal-list'); const terminals = await ipcRenderer.invoke('get-terminal-list');
availableTerminals = terminals || []; availableTerminals = terminals || [];
console.log(`${terminals.length} terminaux récupérés`); console.log(`${terminals.length} terminaux récupérés`);
@@ -1235,36 +1226,50 @@ function refreshCurrentWebview() {
} }
} }
// === GESTION INDICATEUR SIGNALR === // === GESTION INDICATEUR STATUT SERVEUR ===
function updateSignalRIndicator(status) { function updateServerIndicator(status) {
const indicator = document.getElementById('signalrIndicator'); const indicator = document.getElementById('serverIndicator');
const text = document.getElementById('signalrText'); const text = document.getElementById('serverText');
const loginBtn = document.querySelector('#loginForm button[type="submit"]');
const terminalSelect = document.getElementById('terminal');
if (!indicator || !text) return; if (!indicator || !text) return;
// Réinitialiser les classes // Reinitialiser les classes
indicator.className = 'signalr-indicator'; indicator.className = 'server-indicator';
const serverReady = (status === 'connected');
// Activer/desactiver le formulaire de login
if (loginBtn) {
loginBtn.disabled = !serverReady;
loginBtn.textContent = serverReady ? 'Se connecter' : 'Se connecter';
}
if (terminalSelect) {
terminalSelect.disabled = !serverReady;
}
// Mettre a jour Choices.js si present
if (terminalChoices) {
serverReady ? terminalChoices.enable() : terminalChoices.disable();
}
switch(status) { switch(status) {
case 'connected': case 'connected':
indicator.classList.add('connected'); indicator.classList.add('connected');
text.textContent = 'Connecté au serveur'; text.textContent = 'Serveur connecté';
break; break;
case 'connecting': case 'connecting':
indicator.classList.add('connecting'); indicator.classList.add('connecting');
text.textContent = 'Connexion en cours...'; text.textContent = 'Connexion en cours...';
break; break;
case 'disconnected': case 'disconnected':
indicator.classList.add('disconnected');
text.textContent = 'Serveur déconnecté';
break;
case 'error': case 'error':
indicator.classList.add('error'); indicator.classList.add('reconnecting');
text.textContent = 'Erreur de connexion'; text.textContent = 'Reconnexion en cours...';
break; break;
case 'disabled': case 'disabled':
indicator.classList.add('disabled'); indicator.classList.add('disabled');
text.textContent = 'SignalR désactivé'; text.textContent = 'Non configuré';
break; break;
default: default:
text.textContent = 'État inconnu'; text.textContent = 'État inconnu';

View File

@@ -1,285 +0,0 @@
/**
* Adaptateur REST + Socket.IO pour le serveur Python
*
* Utilise REST pour les actions (login, logout, terminaux)
* et Socket.IO pour les événements temps réel (IpbxEvent)
*
* Émule l'API SignalR pour compatibilité avec le code existant
*/
const io = require('socket.io-client');
class RestSocketAdapter {
constructor(serverUrl, options = {}) {
// Nettoyer l'URL
this.serverUrl = serverUrl;
if (this.serverUrl.includes('/signalR')) {
this.serverUrl = this.serverUrl.replace('/signalR', '');
}
if (!this.serverUrl.startsWith('http://') && !this.serverUrl.startsWith('https://')) {
this.serverUrl = `http://${this.serverUrl}`;
}
this.socket = null;
this.connected = false;
this.handlers = new Map();
this.options = options;
// Token JWT pour les requêtes authentifiées
this.authToken = null;
this.currentAgent = null;
}
/**
* Émule connection.start() de SignalR
* Établit la connexion Socket.IO pour les événements
*/
async start() {
return new Promise((resolve, reject) => {
console.log(`[RestSocketAdapter] Connexion à ${this.serverUrl}...`);
// Vérifier d'abord que le serveur Python répond
fetch(`${this.serverUrl}/health`)
.then(response => {
if (!response.ok) {
throw new Error(`Health check failed: ${response.status}`);
}
return response.json();
})
.then(health => {
console.log(`[RestSocketAdapter] Serveur Python détecté:`, health);
// Connecter Socket.IO pour les événements temps réel
this.socket = io(this.serverUrl, {
path: '/socket.io/',
transports: ['websocket', 'polling'],
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 2000,
...this.options
});
this.socket.on('connect', () => {
console.log('[RestSocketAdapter] Socket.IO connecté');
this.connected = true;
resolve();
});
this.socket.on('connect_error', (error) => {
console.error('[RestSocketAdapter] Erreur Socket.IO:', error.message);
// On reste connecté même si Socket.IO échoue (REST fonctionne)
if (!this.connected) {
this.connected = true;
console.log('[RestSocketAdapter] Mode REST seul (Socket.IO indisponible)');
resolve();
}
});
this.socket.on('disconnect', (reason) => {
console.log('[RestSocketAdapter] Socket.IO déconnecté:', reason);
});
// Configurer les handlers d'événements existants
this.handlers.forEach((handler, eventName) => {
this.socket.on(eventName, (...args) => {
console.log(`[RestSocketAdapter] Événement reçu: ${eventName}`, args);
handler(eventName, args);
});
});
// Timeout de connexion
setTimeout(() => {
if (!this.connected) {
reject(new Error('Timeout de connexion au serveur Python'));
}
}, 10000);
})
.catch(error => {
console.error('[RestSocketAdapter] Serveur Python non disponible:', error.message);
reject(error);
});
});
}
/**
* Émule connection.stop() de SignalR
*/
async stop() {
if (this.socket) {
this.socket.disconnect();
}
this.connected = false;
this.authToken = null;
this.currentAgent = null;
console.log('[RestSocketAdapter] Déconnecté');
}
/**
* Émule connection.invoke() de SignalR
* Redirige vers les endpoints REST appropriés
*/
async invoke(methodName, ...args) {
if (!this.connected) {
throw new Error('Non connecté au serveur');
}
console.log(`[RestSocketAdapter] invoke: ${methodName}`, args);
switch (methodName) {
case 'GetTerminalListByServiceProvider':
return this._getTerminalList(args[0]);
case 'AgentLogin':
return this._agentLogin(args[0], args[1], args[2]);
case 'AgentLogoff':
return this._agentLogoff(args[0]);
default:
console.warn(`[RestSocketAdapter] Méthode non implémentée: ${methodName}`);
throw new Error(`Méthode non supportée: ${methodName}`);
}
}
/**
* GET /api/v1/terminals/{serviceProvider}
*/
async _getTerminalList(serviceProvider) {
const response = await fetch(
`${this.serverUrl}/api/v1/terminals/${encodeURIComponent(serviceProvider)}`
);
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: response.statusText }));
throw new Error(error.detail || 'Erreur récupération terminaux');
}
const data = await response.json();
// Retourner juste la liste des numéros de terminaux (format SignalR)
return data.terminals.map(t => t.number || t);
}
/**
* POST /api/v1/auth/login
*/
async _agentLogin(email, password, terminal) {
const response = await fetch(`${this.serverUrl}/api/v1/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
access_code: email,
password: password,
terminal: terminal,
force_disconnect: false
})
});
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: response.statusText }));
throw new Error(error.detail || 'Erreur de connexion');
}
const data = await response.json();
// Stocker le token si présent
if (data.token) {
this.authToken = data.token;
}
this.currentAgent = data;
// Enregistrer le terminal sur Socket.IO pour recevoir les événements
if (this.socket && this.socket.connected) {
this.socket.emit('register', {
terminal: terminal,
agentId: data.accessCode || data.access_code
});
}
// Retourner au format attendu par le client (compatible SignalR)
return {
accessCode: data.accessCode || data.access_code,
firstName: data.firstName || data.first_name,
lastName: data.lastName || data.last_name,
connList: data.connList || data.conn_list || []
};
}
/**
* POST /api/v1/auth/logout
*/
async _agentLogoff(accessCode) {
// Désenregistrer du Socket.IO
if (this.socket && this.socket.connected) {
this.socket.emit('unregister', { terminal: this.currentAgent?.terminal });
}
const headers = {
'Content-Type': 'application/json',
};
// Ajouter le token si disponible
if (this.authToken) {
headers['Authorization'] = `Bearer ${this.authToken}`;
}
const response = await fetch(`${this.serverUrl}/api/v1/auth/logout`, {
method: 'POST',
headers: headers,
body: JSON.stringify({ access_code: accessCode })
});
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: response.statusText }));
throw new Error(error.detail || 'Erreur de déconnexion');
}
this.authToken = null;
this.currentAgent = null;
return { success: true };
}
/**
* Émule connection.on() de SignalR
*/
on(eventName, handler) {
console.log(`[RestSocketAdapter] Enregistrement handler: ${eventName}`);
this.handlers.set(eventName, handler);
// Si Socket.IO est déjà connecté, ajouter le handler
if (this.socket) {
this.socket.on(eventName, (...args) => {
console.log(`[RestSocketAdapter] Événement reçu: ${eventName}`, args);
handler(eventName, args);
});
}
}
/**
* Émule connection.off() de SignalR
*/
off(eventName) {
this.handlers.delete(eventName);
if (this.socket) {
this.socket.off(eventName);
}
}
/**
* État de la connexion (émule SignalR HubConnectionState)
*/
get state() {
return this.connected ? 'Connected' : 'Disconnected';
}
/**
* Identifie ce type d'adaptateur
*/
get isRestSocketAdapter() {
return true;
}
}
module.exports = RestSocketAdapter;

186
socketio-adapter.js Normal file
View File

@@ -0,0 +1,186 @@
/**
* Adaptateur Socket.IO natif pour le serveur Python SimpleServer
*
* Connexion directe au port 8004, auth au handshake,
* reconnexion native illimitee.
*/
const io = require('socket.io-client');
class SocketIOAdapter {
constructor(serverUrl, socketFactory = null) {
this.serverUrl = serverUrl;
this._socketFactory = socketFactory || io;
this.socket = null;
this._state = 'disconnected'; // disconnected, connecting, connected, reconnecting, error
this._eventHandlers = new Map();
this._onStateChange = null; // callback pour notifier main.js des changements d'etat
}
/**
* Enregistrer un callback de changement d'etat.
* @param {function(string)} callback - recoit le nouvel etat
*/
onStateChange(callback) {
this._onStateChange = callback;
}
_setState(state) {
this._state = state;
if (this._onStateChange) {
this._onStateChange(state);
}
}
/**
* Connexion avec auth au handshake.
* Le serveur authentifie dans le handler connect et emet 'login_ok' avec le resultat.
* @returns {Promise<object>} connResult (accessCode, firstName, lastName, connList)
*/
connect(accessCode, password, terminal) {
return new Promise((resolve, reject) => {
this._setState('connecting');
this.socket = this._socketFactory(this.serverUrl, {
auth: { access_code: accessCode, password, terminal },
transports: ['websocket'],
reconnection: true,
reconnectionAttempts: Infinity,
reconnectionDelay: 2000,
reconnectionDelayMax: 10000,
});
let settled = false;
// Le serveur emet 'login_ok' avec les donnees de session
this.socket.once('login_ok', (data) => {
if (settled) return;
settled = true;
this._setState('connected');
resolve(data);
});
// Le serveur emet 'login_error' si auth echouee (avant return false)
this.socket.once('login_error', (data) => {
if (settled) return;
settled = true;
this._setState('error');
this.socket.disconnect();
reject(new Error(data.message || 'Authentification refusee'));
});
// Erreur de connexion (serveur injoignable ou return false du handler connect)
this.socket.on('connect_error', (err) => {
if (!settled) {
settled = true;
this._setState('error');
this.socket.disconnect();
reject(new Error(err.message || 'Connexion refusee'));
}
// Apres login reussi : reconnexion auto, on passe en 'reconnecting'
});
// Timeout de connexion initiale (15s)
setTimeout(() => {
if (!settled) {
settled = true;
this._setState('error');
this.socket.disconnect();
reject(new Error('Timeout de connexion au serveur'));
}
}, 15000);
// === Handlers de reconnexion (actifs apres le premier login) ===
// Deconnexion temporaire → passer en 'reconnecting'
this.socket.on('disconnect', (reason) => {
if (this._state === 'connected') {
// Deconnexion temporaire, socket.io va retenter
this._setState('reconnecting');
}
});
// Reconnexion reussie → le serveur recoit un nouveau connect avec les memes auth
// et emet 'login_ok' (reprise de session)
this.socket.on('login_ok', (data) => {
if (settled && this._state === 'reconnecting') {
this._setState('connected');
}
});
// Restaurer les handlers enregistres avant connect
this._eventHandlers.forEach((handler, event) => {
this.socket.on(event, handler);
});
});
}
/**
* Deconnexion volontaire avec logoff IPBX.
* Emet 'logout' et attend 'logout_ok' du serveur.
*/
logoff() {
return new Promise((resolve) => {
if (!this.socket || !this.socket.connected) {
this._setState('disconnected');
resolve();
return;
}
this.socket.once('logout_ok', () => {
this._setState('disconnected');
resolve();
});
this.socket.emit('logout');
// Timeout si le serveur ne repond pas
setTimeout(() => {
if (this.socket) {
this.socket.disconnect();
}
this._setState('disconnected');
resolve();
}, 5000);
});
}
/**
* Deconnexion brute (sans logoff IPBX).
*/
disconnect() {
if (this.socket) {
this.socket.disconnect();
}
this._setState('disconnected');
}
/**
* Ecouter un evenement serveur.
*/
on(event, handler) {
this._eventHandlers.set(event, handler);
if (this.socket) {
this.socket.on(event, handler);
}
}
/**
* Retirer un handler d'evenement.
*/
off(event) {
this._eventHandlers.delete(event);
if (this.socket) {
this.socket.off(event);
}
}
/**
* Etat de la connexion.
*/
get state() {
return this._state;
}
}
module.exports = SocketIOAdapter;

131
socketio-adapter.test.js Normal file
View File

@@ -0,0 +1,131 @@
import { describe, test, expect, mock, beforeEach } from "bun:test";
const SocketIOAdapter = require("./socketio-adapter.js");
// EventEmitter minimal pour simuler un socket.io
function createFakeSocket() {
const listeners = {};
const socket = {
on(event, fn) { (listeners[event] ??= []).push(fn); },
once(event, fn) {
const wrapper = (...args) => {
socket.off(event, wrapper);
fn(...args);
};
(listeners[event] ??= []).push(wrapper);
},
off(event, fn) {
if (fn) {
listeners[event] = (listeners[event] || []).filter(f => f !== fn);
} else {
delete listeners[event];
}
},
emit: mock(() => {}),
disconnect: mock(() => {}),
connected: true,
// Helper test : déclencher un event côté "serveur"
_fire(event, ...args) {
(listeners[event] || []).slice().forEach(fn => fn(...args));
},
};
return socket;
}
describe("SocketIOAdapter", () => {
let adapter, socket;
beforeEach(() => {
socket = null;
const factory = (url, opts) => {
socket = createFakeSocket();
return socket;
};
adapter = new SocketIOAdapter("http://localhost:8004", factory);
});
test("état initial = disconnected", () => {
expect(adapter.state).toBe("disconnected");
});
test("connect() passe en connecting puis connected sur login_ok", async () => {
const states = [];
adapter.onStateChange((s) => states.push(s));
const p = adapter.connect("1234", "pass", "2001");
socket._fire("login_ok", { accessCode: "1234", firstName: "Test", lastName: "User", connList: [] });
const result = await p;
expect(states).toEqual(["connecting", "connected"]);
expect(adapter.state).toBe("connected");
expect(result.accessCode).toBe("1234");
});
test("connect() rejette sur login_error", async () => {
const p = adapter.connect("1234", "wrong", "2001");
socket._fire("login_error", { message: "Mot de passe incorrect" });
expect(p).rejects.toThrow("Mot de passe incorrect");
expect(adapter.state).toBe("error");
});
test("connect() rejette sur connect_error", async () => {
const p = adapter.connect("1234", "pass", "2001");
socket._fire("connect_error", new Error("Connection refused"));
expect(p).rejects.toThrow("Connection refused");
expect(adapter.state).toBe("error");
});
test("déconnexion temporaire passe en reconnecting", async () => {
const p = adapter.connect("1234", "pass", "2001");
socket._fire("login_ok", { accessCode: "1234" });
await p;
socket._fire("disconnect", "transport close");
expect(adapter.state).toBe("reconnecting");
});
test("reconnexion après déconnexion repasse en connected", async () => {
const p = adapter.connect("1234", "pass", "2001");
socket._fire("login_ok", { accessCode: "1234" });
await p;
socket._fire("disconnect", "transport close");
expect(adapter.state).toBe("reconnecting");
socket._fire("login_ok", { accessCode: "1234" });
expect(adapter.state).toBe("connected");
});
test("logoff() émet logout et passe en disconnected sur logout_ok", async () => {
const p = adapter.connect("1234", "pass", "2001");
socket._fire("login_ok", { accessCode: "1234" });
await p;
const logoffP = adapter.logoff();
socket._fire("logout_ok");
await logoffP;
expect(socket.emit).toHaveBeenCalledWith("logout");
expect(adapter.state).toBe("disconnected");
});
test("onStateChange est appelé à chaque transition", async () => {
const states = [];
adapter.onStateChange((s) => states.push(s));
const p = adapter.connect("1234", "pass", "2001");
socket._fire("login_ok", { accessCode: "1234" });
await p;
socket._fire("disconnect", "transport close");
socket._fire("login_ok", { accessCode: "1234" });
expect(states).toEqual(["connecting", "connected", "reconnecting", "connected"]);
});
});

View File

@@ -121,8 +121,8 @@ body {
font-weight: 400; font-weight: 400;
} }
/* Indicateur de statut SignalR */ /* Indicateur de statut serveur */
.signalr-status { .server-status {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -134,7 +134,7 @@ body {
font-size: 14px; font-size: 14px;
} }
.signalr-indicator { .server-indicator {
width: 12px; width: 12px;
height: 12px; height: 12px;
border-radius: 50%; border-radius: 50%;
@@ -142,22 +142,27 @@ body {
transition: background 0.3s; transition: background 0.3s;
} }
.signalr-indicator.connected { .server-indicator.connected {
background: #4CAF50; background: #4CAF50;
animation: pulse 2s infinite; animation: pulse 2s infinite;
} }
.signalr-indicator.connecting { .server-indicator.connecting {
background: #FFC107; background: #FFC107;
animation: pulse 1s infinite; animation: pulse 1s infinite;
} }
.signalr-indicator.disconnected, .server-indicator.disconnected,
.signalr-indicator.error { .server-indicator.error {
background: #F44336; background: #F44336;
} }
.signalr-indicator.disabled { .server-indicator.reconnecting {
background: #FFC107;
animation: pulse 1.5s infinite;
}
.server-indicator.disabled {
background: #9E9E9E; background: #9E9E9E;
} }
@@ -190,12 +195,19 @@ body {
transition: background 0.3s; transition: background 0.3s;
} }
#loginForm button:hover { #loginForm button:hover:not(:disabled) {
background: #5a6fd8; background: #5a6fd8;
} }
#loginForm button:disabled { #loginForm button:disabled {
background: #a8b0e8; background: #ccc;
color: #999;
cursor: not-allowed;
}
#loginForm select:disabled {
background: #f0f0f0;
color: #999;
cursor: not-allowed; cursor: not-allowed;
} }
@@ -215,96 +227,6 @@ body {
font-size: 14px; font-size: 14px;
} }
/* Checkbox de déconnexion forcée */
.force-disconnect-container {
margin: 25px 0 20px 0;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
transition: all 0.2s ease;
}
.force-disconnect-container:hover {
border-color: #dee2e6;
background: #f1f3f5;
}
.checkbox-label {
display: flex;
align-items: flex-start;
cursor: pointer;
user-select: none;
font-size: 14px;
color: #333;
position: relative;
}
.checkbox-label input[type="checkbox"] {
position: absolute;
opacity: 0;
cursor: pointer;
height: 18px;
width: 18px;
left: 0;
top: 1px;
z-index: 1;
}
.checkbox-label::before {
content: '';
display: inline-block;
width: 18px;
height: 18px;
margin-right: 12px;
margin-top: 1px;
border: 2px solid #dee2e6;
border-radius: 4px;
background: white;
transition: all 0.2s ease;
flex-shrink: 0;
}
.checkbox-label input[type="checkbox"]:checked + span::before {
content: '';
position: absolute;
left: 0;
top: 1px;
width: 18px;
height: 18px;
background: #667eea;
border: 2px solid #667eea;
border-radius: 4px;
}
.checkbox-label input[type="checkbox"]:checked + span::after {
content: '';
position: absolute;
left: 6px;
top: 4px;
width: 6px;
height: 11px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.checkbox-label span {
font-weight: 600;
color: #495057;
letter-spacing: 0.3px;
padding-top: 1px;
}
.checkbox-hint {
display: block;
margin-top: 6px;
margin-left: 30px;
font-size: 13px;
color: #6c757d;
line-height: 1.5;
}
/* === PAGE PRINCIPALE === */ /* === PAGE PRINCIPALE === */
#mainPage { #mainPage {
flex-direction: column; flex-direction: column;

File diff suppressed because one or more lines are too long

View File

@@ -1,168 +0,0 @@
/**
* Adaptateur WebSocket pour fallback quand SignalR n'est pas disponible
* Émule l'API SignalR avec SocketIO
*/
const io = require('socket.io-client');
class WebSocketAdapter {
constructor(serverUrl, options = {}) {
// Nettoyer l'URL et s'assurer qu'elle est valide
this.serverUrl = serverUrl;
if (this.serverUrl.includes('/signalR')) {
this.serverUrl = this.serverUrl.replace('/signalR', '');
}
// S'assurer qu'on a le protocole
if (!this.serverUrl.startsWith('http://') && !this.serverUrl.startsWith('https://')) {
this.serverUrl = `http://${this.serverUrl}`;
}
this.socket = null;
this.connected = false;
this.handlers = new Map();
this.options = options;
this.pendingInvocations = new Map();
this.invocationId = 0;
}
/**
* Émule connection.start() de SignalR
*/
async start() {
return new Promise((resolve, reject) => {
console.log(`🔄 Connexion WebSocket à ${this.serverUrl}...`);
// Se connecter avec SocketIO
this.socket = io(this.serverUrl, {
transports: ['websocket'],
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 2000,
...this.options
});
this.socket.on('connect', () => {
console.log('✅ WebSocket connecté (mode fallback)');
this.connected = true;
// Émuler l'événement 'connected' de SignalR
if (this.handlers.has('connected')) {
this.handlers.get('connected')({ connectionId: this.socket.id });
}
resolve();
});
this.socket.on('connect_error', (error) => {
console.error('❌ Erreur connexion WebSocket:', error.message);
reject(error);
});
this.socket.on('disconnect', (reason) => {
console.log('🔌 WebSocket déconnecté:', reason);
this.connected = false;
});
// Timeout de connexion
setTimeout(() => {
if (!this.connected) {
reject(new Error('Timeout de connexion WebSocket'));
}
}, 10000);
});
}
/**
* Émule connection.stop() de SignalR
*/
async stop() {
if (this.socket) {
this.socket.disconnect();
this.connected = false;
console.log('🛑 WebSocket fermé');
}
}
/**
* Émule connection.invoke() de SignalR
*/
async invoke(methodName, ...args) {
return new Promise((resolve, reject) => {
if (!this.connected) {
reject(new Error('WebSocket non connecté'));
return;
}
const invocationId = ++this.invocationId;
console.log(`📤 WebSocket invoke: ${methodName}`, args);
// Stocker la promesse pour la résolution
this.pendingInvocations.set(invocationId, { resolve, reject });
// Émuler le format SignalR mais utiliser l'API SocketIO
// SocketIO utilise emit avec un callback pour les réponses
this.socket.emit(methodName, ...args, (response) => {
console.log(`📥 WebSocket response for ${methodName}:`, response);
const pending = this.pendingInvocations.get(invocationId);
if (pending) {
pending.resolve(response);
this.pendingInvocations.delete(invocationId);
}
});
// Timeout pour éviter les promesses pendantes
setTimeout(() => {
const pending = this.pendingInvocations.get(invocationId);
if (pending) {
pending.reject(new Error(`Timeout invoking ${methodName}`));
this.pendingInvocations.delete(invocationId);
}
}, 30000);
});
}
/**
* Émule connection.on() de SignalR
*/
on(eventName, handler) {
console.log(`👂 WebSocket listener ajouté: ${eventName}`);
this.handlers.set(eventName, handler);
// Mapper les événements SocketIO vers les handlers
if (this.socket) {
this.socket.on(eventName, (...args) => {
console.log(`📨 WebSocket event reçu: ${eventName}`, args);
handler(eventName, args);
});
}
}
/**
* Émule connection.off() de SignalR
*/
off(eventName) {
this.handlers.delete(eventName);
if (this.socket) {
this.socket.off(eventName);
}
}
/**
* État de la connexion (émule SignalR HubConnectionState)
*/
get state() {
if (this.connected) {
return 'Connected';
}
return 'Disconnected';
}
/**
* Vérifier si la connexion utilise WebSocket (toujours vrai pour cet adaptateur)
*/
get isWebSocketFallback() {
return true;
}
}
module.exports = WebSocketAdapter;