Merge pull request 'feat: migration complète SignalR → Socket.IO (PRD #2)' (#18) from feat/prd-2-socketio-migration into main
This commit is contained in:
@@ -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
|
||||
174
CLAUDE.md
174
CLAUDE.md
@@ -1,149 +1,53 @@
|
||||
# CLAUDE.md
|
||||
# SimpleClient
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
Application desktop de télésecrétariat pour les postes RDVPREM.
|
||||
|
||||
## Commands
|
||||
## Stack
|
||||
- **Electron 28** + **Socket.IO** (socket.io-client 4.8.1) + **Choices.js** 11.1.0
|
||||
- HTML/CSS/JavaScript natif (pas de framework frontend)
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
├── main.js # Process principal — Socket.IO, IPC, fenêtres
|
||||
├── socketio-adapter.js # Adaptateur Socket.IO (connect/logoff/disconnect)
|
||||
├── renderer.js # Process renderer — UI, webviews
|
||||
├── index.html # Structure HTML
|
||||
├── styles-modern.css # Styles CSS
|
||||
├── config.json # Config Socket.IO (serverUrl, serviceProvider)
|
||||
└── notes/ # Stockage notes agents (notes_{agentId}.json)
|
||||
```
|
||||
|
||||
## Commandes
|
||||
|
||||
### Development
|
||||
```bash
|
||||
npm start # Lance l'application en production
|
||||
npm run dev # Mode développement avec DevTools (NODE_ENV=development)
|
||||
bun run dev # Mode dev avec DevTools
|
||||
bun start # Production
|
||||
bun run build # Build toutes plateformes
|
||||
bun run build:linux # Build Linux (AppImage, .deb, .rpm)
|
||||
bun run build:mac # Build macOS (.dmg, .app)
|
||||
```
|
||||
|
||||
### Build & Distribution
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
npm run build # Build pour toutes les plateformes
|
||||
npm run build:win # Build pour Windows (.exe, .nsis)
|
||||
npm run build:mac # Build pour macOS (.dmg, .app)
|
||||
npm run build:linux # Build pour Linux (AppImage, .deb, .rpm)
|
||||
npm run build:linux-x64 # Build Linux spécifique x64
|
||||
npm run build:linux-arm64 # Build Linux spécifique ARM64
|
||||
bun test # 8 tests unitaires socketio-adapter
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Stack technologique
|
||||
- **Electron 28.0.0** : Framework principal pour l'application desktop
|
||||
- **SignalR (@microsoft/signalr 9.0.6)** : Communication temps réel avec le serveur CTI/IPBX
|
||||
- **Choices.js 11.1.0** : Interface de sélection améliorée pour les terminaux téléphoniques
|
||||
- **HTML/CSS/JavaScript natif** : Pas de framework frontend (React, Vue, etc.)
|
||||
|
||||
### Structure des fichiers principaux
|
||||
|
||||
```
|
||||
/
|
||||
├── main.js # Process principal Electron - gère SignalR, IPC et fenêtres
|
||||
├── renderer.js # Process renderer - UI et gestion des webviews
|
||||
├── index.html # Structure HTML de l'application
|
||||
├── styles-modern.css # Styles CSS avec gradients et animations
|
||||
├── config.json # Configuration SignalR uniquement
|
||||
└── notes/ # Stockage des notes agents (format: notes_{agentId}.json)
|
||||
```
|
||||
|
||||
### Communication inter-process (IPC)
|
||||
|
||||
Le système utilise IPC pour la communication entre le process principal et le renderer :
|
||||
|
||||
**Canaux principaux** :
|
||||
- `agent-login` : Authentification via SignalR (AgentLogin)
|
||||
- `logout` / `quit-app` : Déconnexion et fermeture
|
||||
- `get-terminals` : Récupération de la liste des terminaux
|
||||
- `save-notes` / `get-notes` : Gestion des notes avec persistance fichier
|
||||
- `signalr-status` : État de la connexion SignalR
|
||||
- `switch-to-center` / `release-center` : Événements IPBX pour la gestion des appels
|
||||
|
||||
### Intégration SignalR
|
||||
|
||||
**Hub SignalR** : `/planningHub`
|
||||
|
||||
**Méthodes invoquées** :
|
||||
- `GetTerminalListByServiceProvider` : Liste des terminaux disponibles
|
||||
- `AgentLogin` : Connexion agent (email, password, terminal)
|
||||
- `AgentLogoff` : Déconnexion agent
|
||||
|
||||
**Événements écoutés** :
|
||||
- `IpbxEvent` : Événements téléphoniques (codes 1=appel entrant, 2=fin d'appel)
|
||||
- Plus de 13 types d'événements potentiels loggés dans `~/.simpleconnect-ng/signalr.log`
|
||||
|
||||
### Gestion des webviews
|
||||
|
||||
Chaque centre médical a sa propre webview Electron avec :
|
||||
- **Session isolée** : Partition unique par centre pour cookies/auth séparés
|
||||
- **Auto-connexion** : Injection des credentials dans les plateformes (Doctolib, MonDocteur, etc.)
|
||||
- **Preload script** : Injection JavaScript pour automatiser la connexion
|
||||
- **UserAgent personnalisé** : Identification comme navigateur standard
|
||||
|
||||
### Système de logging
|
||||
|
||||
**SignalR logs** : `~/.simpleconnect-ng/signalr.log`
|
||||
- Tous les événements SignalR en JSON structuré
|
||||
- Timestamp, arguments et contexte agent
|
||||
|
||||
**Notes agents** : `/notes/notes_{agentId}.json`
|
||||
- Sauvegarde automatique après 2 secondes d'inactivité
|
||||
- Historique des 50 dernières versions
|
||||
- Synchronisation localStorage + fichier
|
||||
|
||||
## Workflow de développement
|
||||
|
||||
Pour le workflow complet de développement et de release, consulter [.claude/commands/dev.md](.claude/commands/dev.md).
|
||||
|
||||
**Résumé rapide** :
|
||||
1. Créer une branche feature
|
||||
2. Développer en petits commits atomiques (dev → test → commit → repeat)
|
||||
3. Préparer la release (changelog + version bump)
|
||||
4. Merger dans main avec `--no-ff`
|
||||
5. Build et organisation des artefacts
|
||||
6. Documentation et déploiement
|
||||
|
||||
**Points de vigilance** :
|
||||
- Ne JAMAIS committer les binaires dans Git (`dist/` est dans `.gitignore`)
|
||||
- Toujours utiliser `--no-ff` pour les merges (historique tracé)
|
||||
- Tester avant de committer (chaque commit = code fonctionnel)
|
||||
- Suivre Semantic Versioning (semver.org)
|
||||
- Conventional Commits pour les messages (conventionalcommits.org)
|
||||
- `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
|
||||
|
||||
### Sécurité
|
||||
- **Jamais de credentials en dur** : Tout passe par SignalR
|
||||
- **Sessions isolées** : Chaque centre a sa partition Electron
|
||||
- **Logs sécurisés** : Pas de mots de passe dans les logs
|
||||
- **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
|
||||
- **Socket.IO reconnexion** : illimitée (2s→10s backoff)
|
||||
- **IPC principal** : `login-agent`, `get-terminal-list`, `server-status`, `switch-to-center`, `release-center`
|
||||
- **Protocole serveur** : auth au handshake, events `login_ok`/`login_error`/`ipbx_event`/`logout`→`logout_ok`
|
||||
- **Terminaux** : REST `GET /terminals?provider=RDVPREM` (pas Socket.IO)
|
||||
- **Logs** : `~/.simpleconnect-ng/socketio.log`
|
||||
- **Notes** : sauvegarde auto après 2s d'inactivité, 50 versions, sync localStorage + fichier
|
||||
|
||||
### UI/UX
|
||||
- **Pas d'emojis** : Utiliser des icônes SVG inline pour compatibilité Linux
|
||||
- **Animations fluides** : Transitions CSS avec cubic-bezier
|
||||
- **Responsive** : Panneau de notes redimensionnable (280-600px)
|
||||
## Workflow de développement
|
||||
|
||||
### SignalR
|
||||
- **Reconnexion automatique** : [0, 2000, 5000, 10000]ms
|
||||
- **Filtrage par terminal** : Ne recevoir que les événements du terminal connecté
|
||||
- **Gestion des erreurs** : Try/catch sur toutes les invocations
|
||||
|
||||
### Build cross-platform
|
||||
- **Windows** : NSIS installer avec shortcuts
|
||||
- **macOS** : DMG avec icône personnalisée
|
||||
- **Linux** : AppImage, .deb, .rpm (x64 et arm64)
|
||||
|
||||
## Fonctionnalités clés
|
||||
|
||||
### Authentification
|
||||
- Connexion centralisée via SignalR
|
||||
- Option "Forcer la déconnexion" pour débloquer sessions
|
||||
- Auto-focus sur le champ code d'accès
|
||||
|
||||
### Gestion des appels
|
||||
- Basculement automatique vers le bon centre (IpbxEvent code 1)
|
||||
- Libération automatique après raccrochage (IpbxEvent code 2)
|
||||
- Notifications visuelles et sonores
|
||||
|
||||
### Interface
|
||||
- Header unifié avec onglets (fusion pour gagner de l'espace)
|
||||
- Panneau de notes latéral avec sauvegarde automatique
|
||||
- Pas de barre de menu Electron (autoHideMenuBar: true)
|
||||
- Scrollbars masquées mais fonctionnelles
|
||||
|
||||
### Persistance
|
||||
- Notes sauvegardées localement et sur serveur
|
||||
- Préférences utilisateur dans localStorage
|
||||
- Historique des 50 dernières versions de notes
|
||||
Voir [.claude/commands/dev.md](.claude/commands/dev.md) pour le guide complet.
|
||||
|
||||
251
README.md
251
README.md
@@ -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
660
bun.lock
Normal 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=="],
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"signalR": {
|
||||
"enabled": true,
|
||||
"serverUrl": "http://10.90.20.201:8002/signalR",
|
||||
"serviceProvider": "RDVPREM",
|
||||
"terminalsSimulation": ["3001", "3002", "3003", "3004", "3005"]
|
||||
"socketio": {
|
||||
"serverUrl": "http://10.90.20.201:8004",
|
||||
"serviceProvider": "RDVPREM"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
313
cti-simulator.js
313
cti-simulator.js
@@ -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();
|
||||
});
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
24
index.html
24
index.html
@@ -13,9 +13,9 @@
|
||||
<div class="login-container">
|
||||
<h1>SimpleConnect</h1>
|
||||
<div class="app-version-login" id="appVersionLogin"></div>
|
||||
<div class="signalr-status">
|
||||
<span class="signalr-indicator" id="signalrIndicator"></span>
|
||||
<span class="signalr-text" id="signalrText"
|
||||
<div class="server-status">
|
||||
<span class="server-indicator" id="serverIndicator"></span>
|
||||
<span class="server-text" id="serverText"
|
||||
>Connexion au serveur...</span
|
||||
>
|
||||
</div>
|
||||
@@ -34,23 +34,11 @@
|
||||
placeholder="Mot de passe"
|
||||
required
|
||||
/>
|
||||
<select id="terminal" required>
|
||||
<option value="" placeholder>Chargement des postes...</option>
|
||||
<select id="terminal" required disabled>
|
||||
<option value="">En attente du serveur...</option>
|
||||
</select>
|
||||
|
||||
<!-- Option de déconnexion forcée -->
|
||||
<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="submit" disabled>Se connecter</button>
|
||||
<button type="button" id="quitLoginBtn" class="btn-quit">Quitter</button>
|
||||
<div id="loginError" class="error-message"></div>
|
||||
</form>
|
||||
|
||||
677
main.js
677
main.js
@@ -1,51 +1,81 @@
|
||||
const { app, BrowserWindow, ipcMain, session } = require('electron');
|
||||
const { app, BrowserWindow, ipcMain, session, net } = require('electron');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const signalR = require('@microsoft/signalr');
|
||||
const ConnectionManager = require('./connection-manager');
|
||||
const SocketIOAdapter = require('./socketio-adapter');
|
||||
|
||||
let mainWindow;
|
||||
let config;
|
||||
let currentAgent = null;
|
||||
let currentTerminal = null; // Terminal téléphonique de l'agent connecté
|
||||
let signalRConnection = null;
|
||||
let signalRStatus = 'disconnected'; // disconnected, connecting, connected, error
|
||||
let agentConnectionInfo = null; // Informations complètes retournées par SignalR
|
||||
let currentTerminal = null;
|
||||
let adapter = null;
|
||||
let serverStatus = 'disconnected'; // disconnected, connecting, connected, error
|
||||
let agentConnectionInfo = null;
|
||||
let healthCheckInterval = null;
|
||||
|
||||
// Configuration du système de logs SignalR
|
||||
const SIGNALR_LOG_DIR = path.join(os.homedir(), '.simpleconnect-ng');
|
||||
const SIGNALR_LOG_FILE = path.join(SIGNALR_LOG_DIR, 'signalr.log');
|
||||
// Configuration du systeme de logs
|
||||
const LOG_DIR = path.join(os.homedir(), '.simpleconnect-ng');
|
||||
const LOG_FILE = path.join(LOG_DIR, 'socketio.log');
|
||||
|
||||
// Créer le répertoire de logs s'il n'existe pas
|
||||
function ensureLogDirectory() {
|
||||
if (!fs.existsSync(SIGNALR_LOG_DIR)) {
|
||||
fs.mkdirSync(SIGNALR_LOG_DIR, { recursive: true });
|
||||
console.log(`📁 Répertoire de logs créé: ${SIGNALR_LOG_DIR}`);
|
||||
if (!fs.existsSync(LOG_DIR)) {
|
||||
fs.mkdirSync(LOG_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction pour écrire dans le fichier de log SignalR
|
||||
function logToSignalRFile(message, data = null) {
|
||||
function stripAnsi(str) {
|
||||
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();
|
||||
|
||||
const timestamp = new Date().toISOString();
|
||||
let logEntry = `[${timestamp}] ${message}`;
|
||||
|
||||
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');
|
||||
let line = `[${fileTimestamp()}] ${stripAnsi(message)}`;
|
||||
if (data) line += ' ' + JSON.stringify(data);
|
||||
fs.appendFileSync(LOG_FILE, line + '\n', 'utf8');
|
||||
}
|
||||
|
||||
// Logger dans la console ET dans le fichier
|
||||
function logSignalR(message, data = null) {
|
||||
console.log(message, data || '');
|
||||
logToSignalRFile(message, data);
|
||||
function logSectionToFile(message) {
|
||||
ensureLogDirectory();
|
||||
fs.appendFileSync(LOG_FILE, `[${fileTimestamp()}] ── ${stripAnsi(message)} ──\n`, 'utf8');
|
||||
}
|
||||
|
||||
// ANSI colors
|
||||
const c = {
|
||||
reset: '\x1b[0m',
|
||||
dim: '\x1b[2m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
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
|
||||
@@ -55,7 +85,7 @@ function loadConfig() {
|
||||
config = JSON.parse(configData);
|
||||
}
|
||||
|
||||
// Créer la fenêtre principale
|
||||
// Creer la fenetre principale
|
||||
function createWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1400,
|
||||
@@ -68,211 +98,120 @@ function createWindow() {
|
||||
},
|
||||
icon: path.join(__dirname, 'icon.png'),
|
||||
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);
|
||||
|
||||
// Charger l'interface 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.setTitle(`SimpleConnect v${app.getVersion()}`);
|
||||
});
|
||||
|
||||
// Ouvrir les DevTools uniquement en mode développement
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
|
||||
// Gérer la fermeture de la fenêtre
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
}
|
||||
|
||||
// === GESTION SIGNALR/WEBSOCKET ===
|
||||
function initializeSignalR() {
|
||||
if (!config.signalR || !config.signalR.enabled) {
|
||||
console.log('SignalR/WebSocket désactivé dans la configuration');
|
||||
signalRStatus = 'disabled';
|
||||
sendSignalRStatus();
|
||||
// === GESTION SOCKET.IO ===
|
||||
|
||||
function initializeSocketIO() {
|
||||
if (!config.socketio || !config.socketio.serverUrl) {
|
||||
log('Socket.IO non configuré');
|
||||
serverStatus = 'disabled';
|
||||
sendServerStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Utiliser le ConnectionManager avec fallback automatique SignalR → WebSocket
|
||||
const connectionManager = new ConnectionManager(config.ServerIp || config.signalR.serverUrl.replace('http://', '').replace('/signalR', ''));
|
||||
logBanner(app.getVersion(), config.socketio.serverUrl);
|
||||
|
||||
// La connexion sera établie plus tard avec fallback automatique
|
||||
signalRConnection = connectionManager;
|
||||
adapter = new SocketIOAdapter(config.socketio.serverUrl);
|
||||
|
||||
// Les handlers d'état seront configurés après la connexion
|
||||
|
||||
// Démarrer la connexion (les handlers seront configurés après)
|
||||
startSignalRConnection();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur initialisation SignalR:', error);
|
||||
signalRStatus = 'error';
|
||||
sendSignalRStatus();
|
||||
}
|
||||
// Demarrer le health check (ecran de login)
|
||||
startHealthCheck();
|
||||
}
|
||||
|
||||
function setupSignalRHandlers() {
|
||||
// Configuration des handlers après la connexion
|
||||
const connection = signalRConnection.getConnection();
|
||||
if (!connection) {
|
||||
console.error('Pas de connexion active pour configurer les handlers');
|
||||
return;
|
||||
// Health check periodique via GET /health
|
||||
// Actif uniquement sur l'ecran de login (pas d'agent connecte)
|
||||
function startHealthCheck() {
|
||||
const checkHealth = () => {
|
||||
// Ne pas faire de health check quand un agent est connecte
|
||||
// (c'est l'adapter Socket.IO qui pilote le voyant)
|
||||
if (currentAgent) return;
|
||||
|
||||
const serverUrl = config.socketio.serverUrl;
|
||||
let done = false;
|
||||
|
||||
const request = net.request(`${serverUrl}/health`);
|
||||
|
||||
request.on('response', (response) => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
const newStatus = response.statusCode === 200 ? 'connected' : 'error';
|
||||
if (serverStatus !== newStatus) {
|
||||
serverStatus = newStatus;
|
||||
sendServerStatus();
|
||||
}
|
||||
|
||||
// === LOGGER UNIVERSEL POUR TOUS LES MESSAGES SIGNALR ===
|
||||
// Intercepter TOUS les messages reçus du serveur pour découvrir les événements disponibles
|
||||
|
||||
// Initialiser le fichier de log avec une session
|
||||
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
|
||||
const knownEvents = ['IpbxEvent'];
|
||||
|
||||
// Créer un proxy pour intercepter tous les appels .on()
|
||||
const originalOn = signalRConnection.on.bind(signalRConnection);
|
||||
|
||||
// Logger tous les événements possibles en essayant d'écouter les plus communs
|
||||
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('═══════════════════════════════════════════════════════════');
|
||||
request.on('error', () => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
if (serverStatus !== 'error') {
|
||||
serverStatus = 'error';
|
||||
sendServerStatus();
|
||||
}
|
||||
});
|
||||
|
||||
// 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
|
||||
// Timeout 5s — net.request n'a pas de timeout natif
|
||||
setTimeout(() => {
|
||||
if (!done) {
|
||||
done = true;
|
||||
request.abort();
|
||||
if (serverStatus !== 'error') {
|
||||
serverStatus = 'error';
|
||||
sendServerStatus();
|
||||
}
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
request.end();
|
||||
};
|
||||
|
||||
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
|
||||
function handleIpbxEventOriginal(args) {
|
||||
if (!args || !agentConnectionInfo) return;
|
||||
|
||||
const [name, eventArgs] = args;
|
||||
const event = eventArgs?.[0] || args[0];
|
||||
|
||||
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
|
||||
if (event.terminal !== currentTerminal) {
|
||||
console.log('⚠️ Événement ignoré - Terminal différent:', event.terminal, '!==', currentTerminal);
|
||||
return;
|
||||
}
|
||||
|
||||
// Gérer les différents types d'événements
|
||||
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
|
||||
console.log('📋 MÉTHODES SIGNALR DISPONIBLES POUR INVOCATION:');
|
||||
console.log('- AgentLogin(email, password, terminal)');
|
||||
console.log('- AgentLogoff(accessCode)');
|
||||
console.log('- GetTerminalListByServiceProvider(serviceProvider)');
|
||||
console.log('ℹ️ D\'autres méthodes peuvent être disponibles sur le serveur');
|
||||
// Check immediat puis toutes les 5s
|
||||
checkHealth();
|
||||
healthCheckInterval = setInterval(checkHealth, 5000);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if (!mainWindow || !agentConnectionInfo) return;
|
||||
|
||||
// Identifier le centre correspondant à la file
|
||||
const centres = processApplicationUrls(agentConnectionInfo.connList);
|
||||
const centre = centres.find(c => c.queueName === event.queueName);
|
||||
|
||||
if (centre) {
|
||||
console.log('Basculement vers le centre:', centre.nom);
|
||||
|
||||
// Envoyer l'instruction de basculement à la fenêtre
|
||||
log(`Basculement vers le centre: ${centre.nom}`);
|
||||
mainWindow.webContents.send('switch-to-center', {
|
||||
centreId: centre.id,
|
||||
centreName: centre.nom,
|
||||
@@ -281,17 +220,15 @@ function handleCallPickedUp(event) {
|
||||
eventType: 'call_pickup'
|
||||
});
|
||||
} 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) {
|
||||
if (!mainWindow) return;
|
||||
|
||||
console.log('Fin d\'appel sur la file:', event.queueName);
|
||||
|
||||
// Envoyer l'instruction de libération à la fenêtre
|
||||
log(`Fin d'appel sur la file: ${event.queueName}`);
|
||||
mainWindow.webContents.send('release-center', {
|
||||
queueName: event.queueName,
|
||||
terminal: event.terminal,
|
||||
@@ -299,68 +236,45 @@ function handleCallHungUp(event) {
|
||||
});
|
||||
}
|
||||
|
||||
async function startSignalRConnection() {
|
||||
try {
|
||||
signalRStatus = 'connecting';
|
||||
sendSignalRStatus();
|
||||
logSignalR('🔌 Tentative de connexion au serveur...', {
|
||||
serverUrl: config.ServerIp || config.signalR.serverUrl,
|
||||
status: 'connecting'
|
||||
// Configurer les handlers d'evenements Socket.IO apres connexion
|
||||
function setupEventHandlers() {
|
||||
if (!adapter) return;
|
||||
|
||||
log('Session Socket.IO démarrée', {
|
||||
serverUrl: config.socketio.serverUrl,
|
||||
serviceProvider: config.socketio.serviceProvider
|
||||
});
|
||||
|
||||
// Le ConnectionManager gère le fallback automatiquement
|
||||
const connection = await signalRConnection.connect();
|
||||
// Ecouter les evenements d'appels IPBX
|
||||
adapter.on('ipbx_event', (data) => {
|
||||
log('ipbx_event recu', data);
|
||||
|
||||
// Déterminer quel type de connexion a réussi
|
||||
const connectionInfo = signalRConnection.getConnectionInfo();
|
||||
if (!agentConnectionInfo) return;
|
||||
|
||||
console.log(`Connexion établie via ${connectionInfo.type}`);
|
||||
logSignalR(`✅ Connexion établie avec succès (${connectionInfo.type})`, {
|
||||
connectionType: connectionInfo.type,
|
||||
isSignalR: connectionInfo.isSignalR,
|
||||
isRestSocketIO: connectionInfo.isRestSocketIO,
|
||||
status: 'connected',
|
||||
serverUrl: connectionInfo.serverUrl
|
||||
// Verifier que l'evenement est pour notre terminal
|
||||
if (data.terminal !== currentTerminal) {
|
||||
log(`Événement ignoré — terminal différent: ${data.terminal} !== ${currentTerminal}`);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (data.eventCode) {
|
||||
case 1:
|
||||
handleCallPickedUp(data);
|
||||
break;
|
||||
case 2:
|
||||
handleCallHungUp(data);
|
||||
break;
|
||||
default:
|
||||
log('Code evenement non gere:', data);
|
||||
}
|
||||
});
|
||||
|
||||
// Maintenant configurer les handlers sur la connexion active
|
||||
setupSignalRHandlers();
|
||||
|
||||
signalRStatus = 'connected';
|
||||
sendSignalRStatus();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur connexion SignalR:', error);
|
||||
logSignalR('❌ Erreur de connexion SignalR', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
serverUrl: config.signalR.serverUrl
|
||||
});
|
||||
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
|
||||
app.whenReady().then(() => {
|
||||
// Supprimer le menu de l'application complètement (toutes plateformes)
|
||||
const { Menu } = require('electron');
|
||||
Menu.setApplicationMenu(null);
|
||||
|
||||
// Configuration de la session pour éviter les problèmes CORS
|
||||
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';
|
||||
callback({ requestHeaders: details.requestHeaders });
|
||||
@@ -377,33 +291,31 @@ app.whenReady().then(() => {
|
||||
|
||||
loadConfig();
|
||||
createWindow();
|
||||
|
||||
// Initialiser SignalR après le chargement de la config
|
||||
initializeSignalR();
|
||||
initializeSocketIO();
|
||||
});
|
||||
|
||||
// Quitter quand toutes les fenêtres sont fermées
|
||||
// Quitter quand toutes les fenetres sont fermees
|
||||
app.on('window-all-closed', async () => {
|
||||
// Déconnexion propre avant de quitter
|
||||
if (currentAgent && signalRConnection && signalRStatus === 'connected') {
|
||||
try {
|
||||
await signalRConnection.invoke('AgentLogoff', currentAgent.accessCode);
|
||||
console.log('Déconnexion agent avant fermeture');
|
||||
} catch (error) {
|
||||
console.error('Erreur déconnexion:', error);
|
||||
}
|
||||
if (healthCheckInterval) {
|
||||
clearInterval(healthCheckInterval);
|
||||
}
|
||||
|
||||
// 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
|
||||
// Déconnexion propre avant de quitter
|
||||
if (currentAgent && adapter) {
|
||||
try {
|
||||
await adapter.logoff();
|
||||
} catch (error) {
|
||||
log(`${c.red}Erreur déconnexion: ${error.message}${c.reset}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
log(`${c.dim}Application fermée${c.reset}`);
|
||||
logSectionToFile('Application fermée');
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
// Réactiver l'app sur macOS
|
||||
app.on('activate', () => {
|
||||
if (mainWindow === null) {
|
||||
createWindow();
|
||||
@@ -412,108 +324,119 @@ app.on('activate', () => {
|
||||
|
||||
// === IPC HANDLERS ===
|
||||
|
||||
// Obtenir la configuration
|
||||
ipcMain.handle('get-config', () => {
|
||||
return config;
|
||||
});
|
||||
|
||||
// Obtenir la version de l'application
|
||||
ipcMain.handle('get-app-version', () => {
|
||||
return app.getVersion();
|
||||
});
|
||||
|
||||
// Obtenir le statut SignalR
|
||||
ipcMain.handle('get-signalr-status', () => {
|
||||
return signalRStatus;
|
||||
ipcMain.handle('get-server-status', () => {
|
||||
return serverStatus;
|
||||
});
|
||||
|
||||
// Récupérer la liste des terminaux téléphoniques
|
||||
// Recuperer la liste des terminaux via REST
|
||||
ipcMain.handle('get-terminal-list', async () => {
|
||||
// Mode simulation si SignalR non connecté
|
||||
if (!signalRConnection || signalRStatus !== 'connected') {
|
||||
console.log('SignalR non connecté, utilisation des terminaux de simulation');
|
||||
return config.signalR.terminalsSimulation || ['3001', '3002', '3003'];
|
||||
if (!config.socketio || !config.socketio.serverUrl) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Récupération des terminaux pour:', config.signalR.serviceProvider);
|
||||
logSignalR('📡 Invocation SignalR: GetTerminalListByServiceProvider', {
|
||||
method: 'GetTerminalListByServiceProvider',
|
||||
serviceProvider: config.signalR.serviceProvider
|
||||
const provider = config.socketio.serviceProvider;
|
||||
const url = `${config.socketio.serverUrl}/terminals?provider=${encodeURIComponent(provider)}`;
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = net.request(url);
|
||||
let body = '';
|
||||
|
||||
request.on('response', (response) => {
|
||||
response.on('data', (chunk) => {
|
||||
body += chunk.toString();
|
||||
});
|
||||
|
||||
const terminals = await signalRConnection.invoke(
|
||||
'GetTerminalListByServiceProvider',
|
||||
config.signalR.serviceProvider
|
||||
);
|
||||
|
||||
console.log('Terminaux disponibles:', terminals);
|
||||
logSignalR('📞 Terminaux récupérés', {
|
||||
count: terminals.length,
|
||||
terminals: terminals
|
||||
response.on('end', () => {
|
||||
if (response.statusCode === 200) {
|
||||
try {
|
||||
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) {
|
||||
console.error('Erreur récupération terminaux:', error);
|
||||
// Retourner les terminaux de simulation en cas d'erreur
|
||||
return config.signalR.terminalsSimulation || ['3001', '3002', '3003'];
|
||||
log(`${c.red}Erreur récupération terminaux: ${error.message}${c.reset}`);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
// Connexion agent via SignalR
|
||||
// Connexion agent via Socket.IO
|
||||
ipcMain.handle('login-agent', async (event, credentials) => {
|
||||
// Vérifier que SignalR est connecté
|
||||
if (!signalRConnection || signalRStatus !== 'connected') {
|
||||
if (!adapter) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Connexion au serveur SignalR non établie. Veuillez réessayer.'
|
||||
message: 'Socket.IO non initialise. Veuillez reessayer.'
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
console.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
|
||||
});
|
||||
log(`Tentative de connexion agent: ${credentials.email} Terminal: ${credentials.terminal}`);
|
||||
|
||||
// Si déconnexion forcée demandée, déconnecter d'abord la session précédente
|
||||
if (credentials.forceDisconnect) {
|
||||
console.log('Déconnexion forcée demandée pour:', credentials.email);
|
||||
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
|
||||
}
|
||||
// Deconnecter l'adapter precedent s'il y en a un
|
||||
if (adapter.state === 'connected') {
|
||||
adapter.disconnect();
|
||||
}
|
||||
|
||||
// Appel SignalR pour l'authentification
|
||||
logSignalR('📡 Invocation SignalR: AgentLogin', {
|
||||
method: 'AgentLogin',
|
||||
email: credentials.email,
|
||||
terminal: credentials.terminal
|
||||
// Recreer l'adapter pour une connexion fraiche
|
||||
adapter = new SocketIOAdapter(config.socketio.serverUrl);
|
||||
|
||||
// L'adapter pilote le voyant une fois connecte
|
||||
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.password,
|
||||
credentials.terminal
|
||||
);
|
||||
|
||||
if (result) {
|
||||
console.log('Connexion réussie:', result);
|
||||
logSignalR('✅ Connexion agent réussie', {
|
||||
log(`${c.green}✓${c.reset} Connexion réussie: ${result.accessCode} (${result.firstName} ${result.lastName})`);
|
||||
log('Connexion agent réussie', {
|
||||
accessCode: result.accessCode,
|
||||
firstName: result.firstName,
|
||||
lastName: result.lastName,
|
||||
@@ -522,11 +445,9 @@ ipcMain.handle('login-agent', async (event, credentials) => {
|
||||
connList: result.connList
|
||||
});
|
||||
|
||||
// Stocker les informations de connexion
|
||||
agentConnectionInfo = result;
|
||||
currentTerminal = credentials.terminal;
|
||||
|
||||
// Créer l'objet agent pour compatibilité
|
||||
currentAgent = {
|
||||
id: result.accessCode,
|
||||
accessCode: result.accessCode,
|
||||
@@ -537,16 +458,19 @@ ipcMain.handle('login-agent', async (event, credentials) => {
|
||||
terminal: credentials.terminal
|
||||
};
|
||||
|
||||
// Traiter les URLs des applications et créer les centres
|
||||
const centres = processApplicationUrls(result.connList);
|
||||
|
||||
// Mettre à jour le titre de la fenêtre
|
||||
if (mainWindow) {
|
||||
mainWindow.setTitle(
|
||||
`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 {
|
||||
success: true,
|
||||
agent: currentAgent,
|
||||
@@ -554,10 +478,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) {
|
||||
console.error('Erreur lors de la connexion agent:', error);
|
||||
log(`${c.red}Erreur connexion agent: ${error.message}${c.reset}`);
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || 'Erreur de connexion au serveur'
|
||||
@@ -580,7 +504,7 @@ function processApplicationUrls(connList) {
|
||||
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.startsWith('http')) {
|
||||
url = 'https://pro.mondocteur.fr/backoffice.do';
|
||||
@@ -590,11 +514,9 @@ function processApplicationUrls(connList) {
|
||||
url = 'https://pro.doctolib.fr/signin';
|
||||
}
|
||||
} else if (!url.startsWith('http')) {
|
||||
// Ajouter https:// par défaut si pas de protocole
|
||||
url = `https://${url}`;
|
||||
}
|
||||
|
||||
// Créer l'objet centre compatible avec l'interface
|
||||
return {
|
||||
id: conn.code || `centre${index + 1}`,
|
||||
nom: conn.queueName || conn.code || `Centre ${index + 1}`,
|
||||
@@ -604,43 +526,41 @@ function processApplicationUrls(connList) {
|
||||
username: conn.accessCode,
|
||||
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) {
|
||||
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#DDA0DD', '#FFD93D', '#6BCB77'];
|
||||
return colors[index % colors.length];
|
||||
}
|
||||
|
||||
// Déconnexion agent via SignalR
|
||||
// Deconnexion agent via Socket.IO
|
||||
ipcMain.handle('logout', async () => {
|
||||
if (currentAgent && signalRConnection && signalRStatus === 'connected') {
|
||||
if (currentAgent && adapter) {
|
||||
try {
|
||||
// Appeler SignalR pour la déconnexion
|
||||
logSignalR('📡 Invocation SignalR: AgentLogoff', {
|
||||
method: 'AgentLogoff',
|
||||
log('Logoff agent', {
|
||||
accessCode: currentAgent.accessCode
|
||||
});
|
||||
await signalRConnection.invoke('AgentLogoff', currentAgent.accessCode);
|
||||
console.log('Agent déconnecté du serveur SignalR');
|
||||
logSignalR('👋 Agent déconnecté avec succès', {
|
||||
await adapter.logoff();
|
||||
log('Agent déconnecté du serveur');
|
||||
log('Agent déconnecté avec succès', {
|
||||
accessCode: currentAgent.accessCode,
|
||||
name: currentAgent.name
|
||||
});
|
||||
} 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;
|
||||
currentTerminal = null;
|
||||
agentConnectionInfo = null;
|
||||
|
||||
// Réinitialiser le titre de la fenêtre
|
||||
// Retour ecran login → relancer le health check
|
||||
startHealthCheck();
|
||||
|
||||
if (mainWindow) {
|
||||
mainWindow.setTitle(`SimpleConnect v${app.getVersion()}`);
|
||||
}
|
||||
@@ -648,22 +568,15 @@ ipcMain.handle('logout', async () => {
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
// Handler pour quitter l'application proprement
|
||||
ipcMain.handle('quit-app', async () => {
|
||||
// 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
|
||||
// (comme le fait le client de prod)
|
||||
|
||||
// Quitter l'application
|
||||
log(`${c.dim}Application fermée${c.reset}`);
|
||||
logSectionToFile('Application fermée');
|
||||
app.quit();
|
||||
});
|
||||
|
||||
// Obtenir l'agent actuel
|
||||
ipcMain.handle('get-current-agent', () => {
|
||||
if (!currentAgent || !agentConnectionInfo) return null;
|
||||
|
||||
// Retourner les centres traités depuis SignalR
|
||||
const centres = processApplicationUrls(agentConnectionInfo.connList);
|
||||
|
||||
return {
|
||||
@@ -673,36 +586,16 @@ ipcMain.handle('get-current-agent', () => {
|
||||
};
|
||||
});
|
||||
|
||||
// Simuler un appel entrant
|
||||
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)
|
||||
// Sauvegarder les notes de l'agent
|
||||
ipcMain.handle('save-notes', (event, noteData) => {
|
||||
const notesDir = path.join(__dirname, 'notes');
|
||||
if (!fs.existsSync(notesDir)) {
|
||||
fs.mkdirSync(notesDir);
|
||||
}
|
||||
|
||||
// Un seul fichier par agent, mis à jour à chaque sauvegarde
|
||||
const fileName = `notes_${currentAgent.id}.json`;
|
||||
const filePath = path.join(notesDir, fileName);
|
||||
|
||||
// Lire l'historique existant si le fichier existe
|
||||
let notesData = {
|
||||
agent: currentAgent.id,
|
||||
agentName: currentAgent.name,
|
||||
@@ -712,11 +605,9 @@ ipcMain.handle('save-notes', (event, noteData) => {
|
||||
history: []
|
||||
};
|
||||
|
||||
// Si le fichier existe, préserver l'historique
|
||||
if (fs.existsSync(filePath)) {
|
||||
try {
|
||||
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) {
|
||||
notesData.history = existingData.history || [];
|
||||
notesData.history.unshift({
|
||||
@@ -724,11 +615,10 @@ ipcMain.handle('save-notes', (event, noteData) => {
|
||||
date: existingData.lastModified,
|
||||
centre: existingData.centre
|
||||
});
|
||||
// Limiter l'historique à 50 entrées
|
||||
notesData.history = notesData.history.slice(0, 50);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lecture notes existantes:', error);
|
||||
log(`${c.red}Erreur lecture notes existantes: ${error.message}${c.reset}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -737,7 +627,6 @@ ipcMain.handle('save-notes', (event, noteData) => {
|
||||
return { success: true, file: fileName };
|
||||
});
|
||||
|
||||
// Récupérer les notes de l'agent
|
||||
ipcMain.handle('get-notes', () => {
|
||||
if (!currentAgent) return null;
|
||||
|
||||
@@ -750,7 +639,7 @@ ipcMain.handle('get-notes', () => {
|
||||
const data = fs.readFileSync(filePath, 'utf8');
|
||||
return JSON.parse(data);
|
||||
} catch (error) {
|
||||
console.error('Erreur lecture notes:', error);
|
||||
log(`${c.red}Erreur lecture notes: ${error.message}${c.reset}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -758,7 +647,6 @@ ipcMain.handle('get-notes', () => {
|
||||
return null;
|
||||
});
|
||||
|
||||
// Obtenir l'historique des appels
|
||||
ipcMain.handle('get-call-history', () => {
|
||||
const historyFile = path.join(__dirname, 'call_history.json');
|
||||
if (fs.existsSync(historyFile)) {
|
||||
@@ -768,7 +656,6 @@ ipcMain.handle('get-call-history', () => {
|
||||
return [];
|
||||
});
|
||||
|
||||
// Sauvegarder un appel dans l'historique
|
||||
ipcMain.handle('save-call-history', (event, callData) => {
|
||||
const historyFile = path.join(__dirname, 'call_history.json');
|
||||
let history = [];
|
||||
@@ -784,14 +671,8 @@ ipcMain.handle('save-call-history', (event, callData) => {
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Garder seulement les 100 derniers appels
|
||||
history = history.slice(0, 100);
|
||||
|
||||
fs.writeFileSync(historyFile, JSON.stringify(history, null, 2));
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
// Vérifier si on est en mode développement
|
||||
ipcMain.handle('is-development', () => {
|
||||
return process.env.NODE_ENV === 'development';
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "simpleconnect-electron",
|
||||
"version": "1.5.0",
|
||||
"version": "2.0.1",
|
||||
"description": "Application de gestion centralisée des plannings médicaux pour centres d'appels",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
@@ -13,7 +13,8 @@
|
||||
"build:linux-x64": "electron-builder --linux --x64",
|
||||
"build:linux-arm64": "electron-builder --linux --arm64",
|
||||
"dist": "electron-builder",
|
||||
"postinstall": "electron-builder install-app-deps"
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"test": "bun test"
|
||||
},
|
||||
"keywords": [
|
||||
"electron",
|
||||
@@ -85,7 +86,6 @@
|
||||
"url": "https://github.com/simpleconnect/electron-app.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/signalr": "^9.0.6",
|
||||
"choices.js": "^11.1.0",
|
||||
"socket.io-client": "^4.8.1"
|
||||
}
|
||||
|
||||
@@ -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*
|
||||
@@ -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*
|
||||
@@ -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)
|
||||
93
renderer.js
93
renderer.js
@@ -29,21 +29,26 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
versionLoginElement.textContent = `v${appVersion}`;
|
||||
}
|
||||
|
||||
// Initialiser l'indicateur SignalR
|
||||
// Initialiser l'indicateur de statut serveur
|
||||
|
||||
// Écouter les changements de statut SignalR
|
||||
ipcRenderer.on('signalr-status', (event, status) => {
|
||||
updateSignalRIndicator(status);
|
||||
// Recharger les terminaux à chaque changement de statut
|
||||
// Ecouter les changements de statut serveur
|
||||
let previousServerStatus = null;
|
||||
ipcRenderer.on('server-status', (event, status) => {
|
||||
updateServerIndicator(status);
|
||||
// Recharger les terminaux uniquement quand la connexion (re)monte
|
||||
if (status === 'connected' && previousServerStatus !== 'connected') {
|
||||
loadTerminals();
|
||||
}
|
||||
previousServerStatus = status;
|
||||
});
|
||||
|
||||
// Obtenir le statut initial SignalR
|
||||
const initialStatus = await ipcRenderer.invoke('get-signalr-status');
|
||||
updateSignalRIndicator(initialStatus);
|
||||
|
||||
// Charger immédiatement les terminaux pour la page de login
|
||||
await loadTerminals();
|
||||
// Obtenir le statut initial
|
||||
const initialStatus = await ipcRenderer.invoke('get-server-status');
|
||||
updateServerIndicator(initialStatus);
|
||||
if (initialStatus === 'connected') {
|
||||
loadTerminals();
|
||||
}
|
||||
previousServerStatus = initialStatus;
|
||||
|
||||
// Vérifier si un agent est déjà connecté
|
||||
const agentData = await ipcRenderer.invoke('get-current-agent');
|
||||
@@ -129,7 +134,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
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) => {
|
||||
console.log('Basculement vers le centre:', data.centreName);
|
||||
|
||||
@@ -162,14 +167,13 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Connexion via SignalR
|
||||
// Connexion agent
|
||||
async function handleLogin(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const accessCode = document.getElementById('accessCode').value;
|
||||
const password = document.getElementById('password').value;
|
||||
const terminal = document.getElementById('terminal').value;
|
||||
const forceDisconnect = document.getElementById('forceDisconnect').checked;
|
||||
const errorDiv = document.getElementById('loginError');
|
||||
const loginBtn = document.querySelector('#loginForm button[type="submit"]');
|
||||
|
||||
@@ -210,26 +214,25 @@ async function handleLogin(e) {
|
||||
localStorage.setItem('last-terminal', terminal);
|
||||
|
||||
// Afficher la modal de progression de connexion
|
||||
showLoginProgress(forceDisconnect);
|
||||
showLoginProgress();
|
||||
|
||||
// Désactiver le bouton pendant la connexion
|
||||
loginBtn.disabled = true;
|
||||
loginBtn.textContent = forceDisconnect ? 'Reconnexion...' : 'Connexion en cours...';
|
||||
loginBtn.textContent = 'Connexion en cours...';
|
||||
errorDiv.textContent = '';
|
||||
|
||||
// Attendre un peu pour que l'animation soit visible
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
|
||||
try {
|
||||
// Préparer les credentials pour SignalR
|
||||
// Preparer les credentials
|
||||
const credentials = {
|
||||
email: accessCode, // Utiliser directement le code agent comme email
|
||||
password: password,
|
||||
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);
|
||||
|
||||
if (result.success) {
|
||||
@@ -423,25 +426,17 @@ function resetLoginForm() {
|
||||
const accessCode = document.getElementById('accessCode');
|
||||
const password = document.getElementById('password');
|
||||
const terminal = document.getElementById('terminal');
|
||||
const forceDisconnect = document.getElementById('forceDisconnect');
|
||||
const loginError = document.getElementById('loginError');
|
||||
const loginBtn = document.querySelector('#loginForm button[type="submit"]');
|
||||
|
||||
if (accessCode) accessCode.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
|
||||
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) {
|
||||
loginBtn.disabled = false;
|
||||
loginBtn.textContent = 'Se connecter';
|
||||
}
|
||||
}
|
||||
@@ -835,7 +830,7 @@ async function loadTerminals() {
|
||||
console.log('Chargement des terminaux...');
|
||||
|
||||
try {
|
||||
// Récupérer les terminaux depuis le serveur SignalR
|
||||
// Recuperer les terminaux depuis le serveur
|
||||
const terminals = await ipcRenderer.invoke('get-terminal-list');
|
||||
availableTerminals = terminals || [];
|
||||
console.log(`${terminals.length} terminaux récupérés`);
|
||||
@@ -1235,36 +1230,50 @@ function refreshCurrentWebview() {
|
||||
}
|
||||
}
|
||||
|
||||
// === GESTION INDICATEUR SIGNALR ===
|
||||
function updateSignalRIndicator(status) {
|
||||
const indicator = document.getElementById('signalrIndicator');
|
||||
const text = document.getElementById('signalrText');
|
||||
// === GESTION INDICATEUR STATUT SERVEUR ===
|
||||
function updateServerIndicator(status) {
|
||||
const indicator = document.getElementById('serverIndicator');
|
||||
const text = document.getElementById('serverText');
|
||||
const loginBtn = document.querySelector('#loginForm button[type="submit"]');
|
||||
const terminalSelect = document.getElementById('terminal');
|
||||
|
||||
if (!indicator || !text) return;
|
||||
|
||||
// Réinitialiser les classes
|
||||
indicator.className = 'signalr-indicator';
|
||||
// Reinitialiser les classes
|
||||
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) {
|
||||
case 'connected':
|
||||
indicator.classList.add('connected');
|
||||
text.textContent = 'Connecté au serveur';
|
||||
text.textContent = 'Serveur connecté';
|
||||
break;
|
||||
case 'connecting':
|
||||
indicator.classList.add('connecting');
|
||||
text.textContent = 'Connexion en cours...';
|
||||
break;
|
||||
case 'disconnected':
|
||||
indicator.classList.add('disconnected');
|
||||
text.textContent = 'Serveur déconnecté';
|
||||
break;
|
||||
case 'error':
|
||||
indicator.classList.add('error');
|
||||
text.textContent = 'Erreur de connexion';
|
||||
indicator.classList.add('reconnecting');
|
||||
text.textContent = 'Reconnexion en cours...';
|
||||
break;
|
||||
case 'disabled':
|
||||
indicator.classList.add('disabled');
|
||||
text.textContent = 'SignalR désactivé';
|
||||
text.textContent = 'Non configuré';
|
||||
break;
|
||||
default:
|
||||
text.textContent = 'État inconnu';
|
||||
|
||||
@@ -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
186
socketio-adapter.js
Normal 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;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
if (this.socket) {
|
||||
this.socket.disconnect();
|
||||
}
|
||||
this._setState('disconnected');
|
||||
resolve();
|
||||
}, 5000);
|
||||
|
||||
this.socket.once('logout_ok', () => {
|
||||
clearTimeout(timeout);
|
||||
this._setState('disconnected');
|
||||
resolve();
|
||||
});
|
||||
|
||||
this.socket.emit('logout');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
131
socketio-adapter.test.js
Normal 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"]);
|
||||
});
|
||||
});
|
||||
@@ -121,8 +121,8 @@ body {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* Indicateur de statut SignalR */
|
||||
.signalr-status {
|
||||
/* Indicateur de statut serveur */
|
||||
.server-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -134,7 +134,7 @@ body {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.signalr-indicator {
|
||||
.server-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
@@ -142,22 +142,27 @@ body {
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.signalr-indicator.connected {
|
||||
.server-indicator.connected {
|
||||
background: #4CAF50;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.signalr-indicator.connecting {
|
||||
.server-indicator.connecting {
|
||||
background: #FFC107;
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
.signalr-indicator.disconnected,
|
||||
.signalr-indicator.error {
|
||||
.server-indicator.disconnected,
|
||||
.server-indicator.error {
|
||||
background: #F44336;
|
||||
}
|
||||
|
||||
.signalr-indicator.disabled {
|
||||
.server-indicator.reconnecting {
|
||||
background: #FFC107;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
.server-indicator.disabled {
|
||||
background: #9E9E9E;
|
||||
}
|
||||
|
||||
@@ -190,12 +195,19 @@ body {
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
#loginForm button:hover {
|
||||
#loginForm button:hover:not(:disabled) {
|
||||
background: #5a6fd8;
|
||||
}
|
||||
|
||||
#loginForm button:disabled {
|
||||
background: #a8b0e8;
|
||||
background: #ccc;
|
||||
color: #999;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
#loginForm select:disabled {
|
||||
background: #f0f0f0;
|
||||
color: #999;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@@ -215,96 +227,6 @@ body {
|
||||
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 === */
|
||||
#mainPage {
|
||||
flex-direction: column;
|
||||
|
||||
961
styles.css
961
styles.css
File diff suppressed because one or more lines are too long
@@ -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;
|
||||
Reference in New Issue
Block a user