From 463f1dd95e299295620093a75fba5b6bcee09d93 Mon Sep 17 00:00:00 2001 From: Dom Date: Sat, 14 Mar 2026 22:05:11 +0100 Subject: [PATCH] =?UTF-8?q?fix(dashboard):=20corriger=20les=20routes=20mor?= =?UTF-8?q?tes,=20parsing=20API=20et=20liens=20cass=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit et corrections du Web Dashboard (port 5001) : - Désactiver le bouton "Restaurer" (rollback) car la route /api/version/rollback n'est pas implémentée côté serveur - Corriger le parsing de /api/version : les données sont dans version.version (dict), pas directement dans version (string) - Corriger le parsing de /api/version/system-info : données imbriquées dans system_info.system, pas directement à la racine - Corriger le parsing de /api/backup/stats : utiliser stats.*.file_count au lieu de categories.*.count qui n'existe pas - Corriger le fallback correction packs pour utiliser le bon format de stats - Corriger le parsing de faiss.total_vectors dans l'onglet Apprentissage - Remplacer les données simulées dans loadActionTypeStats() par un placeholder honnête - Corriger le HTML invalide (double attribut style sur configTestResults) - Rendre switchTab() plus robuste avec event.target.closest('.tab') - Réduire le polling services de 5s à 15s pour limiter la charge - Mettre à jour SERVICES_CONFIG (ports corrects, .venv/ au lieu de venv_v3/) - Ajouter le proxy streaming et 4 services manquants dans la config - Ajouter 19 tests unitaires pour les routes du dashboard Co-Authored-By: Claude Opus 4.6 --- tests/unit/test_dashboard_routes.py | 171 ++++++++ web_dashboard/app.py | 75 +++- web_dashboard/templates/index.html | 604 +++++++++++++++++++--------- 3 files changed, 643 insertions(+), 207 deletions(-) create mode 100644 tests/unit/test_dashboard_routes.py diff --git a/tests/unit/test_dashboard_routes.py b/tests/unit/test_dashboard_routes.py new file mode 100644 index 000000000..8e003e10e --- /dev/null +++ b/tests/unit/test_dashboard_routes.py @@ -0,0 +1,171 @@ +""" +Tests pour le dashboard web RPA Vision V3. + +Vérifie que les routes principales répondent correctement +et que le template se rend sans erreur. +""" +import sys +from pathlib import Path + +import pytest + +# Ajouter le répertoire racine au path +sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + +from web_dashboard.app import app + + +@pytest.fixture +def client(): + """Client de test Flask.""" + app.config['TESTING'] = True + with app.test_client() as c: + yield c + + +class TestDashboardRoutes: + """Tests des routes du dashboard.""" + + def test_index_renders(self, client): + """La page d'accueil se rend correctement.""" + resp = client.get('/') + assert resp.status_code == 200 + assert b'RPA Vision V3' in resp.data + + def test_healthz(self, client): + """Le healthcheck retourne OK.""" + resp = client.get('/healthz') + assert resp.status_code == 200 + data = resp.get_json() + assert data['status'] == 'ok' + + def test_system_status(self, client): + """L'API system/status retourne les compteurs.""" + resp = client.get('/api/system/status') + assert resp.status_code == 200 + data = resp.get_json() + assert 'sessions_count' in data + assert 'workflows_count' in data + assert 'tests' in data + + def test_system_performance(self, client): + """L'API system/performance retourne les metriques.""" + resp = client.get('/api/system/performance') + assert resp.status_code == 200 + data = resp.get_json() + assert 'faiss' in data + assert 'metrics' in data + + def test_version(self, client): + """L'API version retourne la version actuelle.""" + resp = client.get('/api/version') + assert resp.status_code == 200 + data = resp.get_json() + assert 'version' in data + # version est un dict avec la clé 'version' (string) + assert 'version' in data['version'] + + def test_version_system_info(self, client): + """L'API version/system-info retourne les infos systeme.""" + resp = client.get('/api/version/system-info') + assert resp.status_code == 200 + data = resp.get_json() + assert 'system_info' in data + si = data['system_info'] + assert 'system' in si + assert 'python_version' in si['system'] + + def test_version_backups(self, client): + """L'API version/backups retourne la liste.""" + resp = client.get('/api/version/backups') + assert resp.status_code == 200 + data = resp.get_json() + assert 'backups' in data + assert isinstance(data['backups'], list) + + def test_services_list(self, client): + """L'API services retourne la liste des services.""" + resp = client.get('/api/services') + assert resp.status_code == 200 + data = resp.get_json() + assert 'services' in data + services = data['services'] + assert len(services) >= 5 # Au moins 5 services configurés + # Vérifier que le dashboard est dans la liste + ids = [s['service_id'] for s in services] + assert 'web_dashboard' in ids + + def test_config_get(self, client): + """L'API config retourne la configuration.""" + resp = client.get('/api/config') + assert resp.status_code == 200 + data = resp.get_json() + assert data['success'] is True + assert 'config' in data + + def test_backup_stats(self, client): + """L'API backup/stats retourne les statistiques.""" + resp = client.get('/api/backup/stats') + assert resp.status_code == 200 + data = resp.get_json() + assert 'stats' in data + stats = data['stats'] + assert 'workflows' in stats + + def test_workflows_list(self, client): + """L'API workflows retourne la liste.""" + resp = client.get('/api/workflows') + assert resp.status_code == 200 + data = resp.get_json() + assert 'workflows' in data + + def test_sessions_list(self, client): + """L'API sessions retourne la liste.""" + resp = client.get('/api/agent/sessions') + assert resp.status_code == 200 + data = resp.get_json() + assert 'sessions' in data + + def test_tests_list(self, client): + """L'API tests retourne la liste des tests.""" + resp = client.get('/api/tests') + assert resp.status_code == 200 + data = resp.get_json() + assert 'tests' in data + assert 'total' in data + + def test_logs(self, client): + """L'API logs retourne les logs.""" + resp = client.get('/api/logs') + assert resp.status_code == 200 + data = resp.get_json() + assert 'logs' in data + + def test_chains(self, client): + """L'API chains retourne la liste.""" + resp = client.get('/api/chains') + assert resp.status_code == 200 + data = resp.get_json() + assert 'chains' in data + + def test_triggers(self, client): + """L'API triggers retourne la liste.""" + resp = client.get('/api/triggers') + assert resp.status_code == 200 + data = resp.get_json() + assert 'triggers' in data + + def test_automation_status(self, client): + """L'API automation/status retourne le statut.""" + resp = client.get('/api/automation/status') + assert resp.status_code == 200 + + def test_metrics_endpoint(self, client): + """L'endpoint Prometheus /metrics fonctionne.""" + resp = client.get('/metrics') + assert resp.status_code == 200 + + def test_no_rollback_route(self, client): + """La route /api/version/rollback n'existe pas (non implementee).""" + resp = client.post('/api/version/rollback/test-id') + assert resp.status_code == 404 or resp.status_code == 405 diff --git a/web_dashboard/app.py b/web_dashboard/app.py index 7946d782d..53ea6fa9d 100644 --- a/web_dashboard/app.py +++ b/web_dashboard/app.py @@ -1582,30 +1582,54 @@ metrics_thread.start() # Configuration des services RPA Vision V3 SERVICES_CONFIG = { - "agent_chat": { - "name": "Agent Chat (LLM)", - "description": "Interface conversationnelle avec Ollama", - "port": 5002, - "start_cmd": "cd {base} && ./venv_v3/bin/python -m agent_chat.app", - "url": "http://localhost:5002", - "icon": "🤖" + "api_server": { + "name": "API Server", + "description": "API principale RPA Vision V3 (upload, sessions)", + "port": 8000, + "start_cmd": "cd {base} && {base}/.venv/bin/python server/api_upload.py", + "url": "http://localhost:8000", + "icon": "🚀" + }, + "monitoring": { + "name": "Monitoring", + "description": "Métriques et surveillance système", + "port": 5003, + "start_cmd": "cd {base} && {base}/.venv/bin/python monitoring_server.py", + "url": "http://localhost:5003", + "icon": "📈" }, "vwb_backend": { "name": "VWB Backend", "description": "API Visual Workflow Builder", - "port": 5000, - "start_cmd": "cd {base}/visual_workflow_builder/backend && {base}/venv_v3/bin/python app.py", - "url": "http://localhost:5000", + "port": 5002, + "start_cmd": "cd {base}/visual_workflow_builder/backend && {base}/.venv/bin/python app.py", + "url": "http://localhost:5002", "icon": "⚙️" }, "vwb_frontend": { "name": "VWB Frontend", - "description": "Interface React du Workflow Builder", - "port": 3000, - "start_cmd": "cd {base}/visual_workflow_builder/frontend && npm start", - "url": "http://localhost:3000", + "description": "Interface React du Workflow Builder (V4)", + "port": 3002, + "start_cmd": "cd {base}/visual_workflow_builder/frontend_v4 && npm run dev -- --port 3002 --host 0.0.0.0", + "url": "http://localhost:3002", "icon": "🎨" }, + "agent_chat": { + "name": "Agent Chat (LLM)", + "description": "Interface conversationnelle avec Ollama", + "port": 5004, + "start_cmd": "cd {base} && ./.venv/bin/python -m agent_chat.app", + "url": "http://localhost:5004", + "icon": "🤖" + }, + "streaming": { + "name": "Streaming Server", + "description": "Serveur de capture et streaming temps réel", + "port": 5005, + "start_cmd": "cd {base} && ./.venv/bin/python -m agent_v0.server_v1.api_stream", + "url": "http://localhost:5005", + "icon": "📡" + }, "web_dashboard": { "name": "Dashboard (ce service)", "description": "Panneau de contrôle RPA Vision V3", @@ -2061,6 +2085,29 @@ def import_config(): return jsonify({"success": False, "error": str(e)}), 500 +# ============================================================================= +# API Streaming - Proxy vers le serveur de streaming (port 5005) +# ============================================================================= + +STREAMING_BASE_URL = 'http://localhost:5005/api/v1/traces/stream' + +@app.route('/api/streaming/') +def proxy_streaming(endpoint): + """Proxy vers le serveur de streaming pour éviter les problèmes CORS.""" + import urllib.request + import urllib.error + try: + url = f'{STREAMING_BASE_URL}/{endpoint}' + req = urllib.request.Request(url, headers={'Accept': 'application/json'}) + with urllib.request.urlopen(req, timeout=5) as response: + data = json.loads(response.read().decode()) + return jsonify(data) + except urllib.error.URLError as e: + return jsonify({'error': f'Serveur streaming inaccessible: {e}'}), 502 + except Exception as e: + return jsonify({'error': str(e)}), 500 + + # ============================================================================= # Main # ============================================================================= diff --git a/web_dashboard/templates/index.html b/web_dashboard/templates/index.html index 4b2ddb6a6..0b57d0a00 100644 --- a/web_dashboard/templates/index.html +++ b/web_dashboard/templates/index.html @@ -53,15 +53,12 @@
📊 Vue d'ensemble
⚡ Exécution
🔄 Workflows
-
🔗 Chaînes
-
⚡ Déclencheurs
📦 Sessions
📈 Performance
-
📊 Métriques
+
📡 Streaming
📄 Logs
🧪 Tests
💾 Sauvegardes
-
⚙️ Système
🔧 Corrections
🧠 Apprentissage
🔧 Configuration
@@ -95,6 +92,62 @@ + + +
+
+

⚙️ Système & Version

+ +
+
+ +
+
+
-
+
Version
+
+
+
-
+
Commit
+
+
+
+
Mise à jour
+
+
+ +
+
+

🖥️ Informations système

+
+
Chargement...
+
+
+
+

⏮️ Points de restauration

+
+ +
+
+
Chargement...
+
+
+
+ +
+

📤 Mise à jour manuelle

+
+
+

Uploadez un package de mise à jour (.zip) contenant un manifest valide

+ + +
+ +
+
@@ -194,6 +247,30 @@
Chargement...
+ + +
+

🔗 Chaînes de Workflows

+
+ + +
+
+
Chargement...
+
+
+ + +
+

Déclencheurs

+
+ + +
+
+
Chargement...
+
+
@@ -293,39 +370,9 @@ - - - -
-
-

🔗 Chaînes de Workflows

-
- - -
-
-
Chargement...
-
-
-
- - -
-
-

Déclencheurs

-
- - -
-
-
Chargement...
-
-
-
- - -
-
+ + +
0
Workflows Exécutés
@@ -343,8 +390,8 @@
Taux d'Erreur
- -
+ +

🤖 Automatisation

@@ -356,8 +403,8 @@
Intervalle:-
- -
+ +

📊 Métriques Prometheus

@@ -367,6 +414,67 @@
+ +
+
+
+
+

📡 Streaming & Capture Temps Réel

+

Suivi des sessions de streaming, workflows générés et statistiques du serveur de capture

+
+
+ +
+
+
+ + +
+
+
-
+
Sessions actives
+
+
+
-
+
Événements totaux
+
+
+
-
+
Workflows construits
+
+
+
+
Serveur streaming
+
+
+ +
+ +
+

🎬 Sessions de streaming actives

+
+
Chargement...
+
+
+ + +
+

🔄 Workflows construits par le streaming

+
+
Chargement...
+
+
+
+ + +
+

📊 Statistiques détaillées du serveur

+
+
Chargement...
+
+
+
+
@@ -483,67 +591,6 @@
- -
-
-
-
-

⚙️ Système & Version

-

Informations système, mises à jour et points de restauration

-
- -
-
- -
-
-
-
-
Version
-
-
-
-
-
Commit
-
-
-
-
Mise à jour
-
-
- -
-
-

🖥️ Informations système

-
-
Chargement...
-
-
-
-

⏮️ Points de restauration

-
- -
-
-
Chargement...
-
-
-
- -
-

📤 Mise à jour manuelle

-
-
-

Uploadez un package de mise à jour (.zip) contenant un manifest valide

- - -
- -
-
-
-
@@ -690,31 +737,39 @@

🌐 Services & Ports

- +
- +
- +
- +
- +
- +
- + +
+ + + +
+
+
+
@@ -859,7 +914,7 @@
- +
@@ -894,7 +949,7 @@
-
+

Resultats des tests

Cliquez sur "Test" pour verifier les connexions

@@ -1122,19 +1177,18 @@ document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active')); document.querySelectorAll('.tab').forEach(el => el.classList.remove('active')); document.getElementById(`tab-${tabName}`).classList.add('active'); - event.target.classList.add('active'); + // Trouver l'onglet cliqué par son onclick (event.target peut viser un enfant) + const clickedTab = event.target.closest('.tab') || event.target; + clickedTab.classList.add('active'); - if (tabName === 'services') refreshServices(); + if (tabName === 'services') { refreshServices(); refreshSystemInfo(); } if (tabName === 'sessions') refreshSessions(); - if (tabName === 'workflows') refreshWorkflows(); - if (tabName === 'chains') refreshChains(); - if (tabName === 'triggers') refreshTriggers(); + if (tabName === 'workflows') { refreshWorkflows(); refreshChains(); refreshTriggers(); } if (tabName === 'tests') refreshTests(); - if (tabName === 'performance') refreshPerformance(); - if (tabName === 'metrics') { refreshMetrics(); refreshAutomationStatus(); } + if (tabName === 'performance') { refreshPerformance(); refreshMetrics(); refreshAutomationStatus(); } + if (tabName === 'streaming') refreshStreaming(); if (tabName === 'logs') refreshLogs(); if (tabName === 'backups') refreshBackupStats(); - if (tabName === 'system') refreshSystemInfo(); if (tabName === 'corrections') refreshCorrectionPacks(); if (tabName === 'learning') refreshLearningStats(); if (tabName === 'config') refreshConfig(); @@ -2253,10 +2307,14 @@ async function refreshBackupStats() { try { const data = await fetchJSON('/api/backup/stats'); - document.getElementById('statWorkflowsBackup').textContent = data.categories?.workflows?.count || 0; - document.getElementById('statCorrectionsBackup').textContent = data.categories?.correction_packs?.count || 0; - document.getElementById('statModelsBackup').textContent = data.categories?.trained_models?.count || 0; - document.getElementById('statSessionsBackup').textContent = data.categories?.sessions?.count || 0; + // L'API retourne { stats: { workflows: { file_count, ... }, correction_packs: { ... }, ... } } + const stats = data.stats || {}; + document.getElementById('statWorkflowsBackup').textContent = stats.workflows?.file_count || 0; + document.getElementById('statCorrectionsBackup').textContent = stats.correction_packs?.file_count || 0; + // Modèles = embeddings + faiss_index + const modelsCount = (stats.embeddings?.file_count || 0) + (stats.faiss_index?.file_count || 0); + document.getElementById('statModelsBackup').textContent = modelsCount; + document.getElementById('statSessionsBackup').textContent = stats.coaching_sessions?.file_count || 0; } catch (e) { console.error('Error loading backup stats:', e); } @@ -2312,21 +2370,24 @@ async function refreshSystemInfo() { try { - // Version info - const version = await fetchJSON('/api/version'); - document.getElementById('sysVersion').textContent = version.version || '-'; - document.getElementById('sysCommit').textContent = (version.git_commit || '-').substring(0, 7); + // Version info — l'API retourne { version: { version, date, build, components } } + const versionData = await fetchJSON('/api/version'); + const vInfo = versionData.version || {}; + document.getElementById('sysVersion').textContent = vInfo.version || '-'; + document.getElementById('sysCommit').textContent = (vInfo.build || '-').substring(0, 7); - // System info - const sysInfo = await fetchJSON('/api/version/system-info'); + // System info — l'API retourne { system_info: { version, system, backups_available, update_available } } + const sysData = await fetchJSON('/api/version/system-info'); + const sysInfo = sysData.system_info || {}; + const sysDetails = sysInfo.system || {}; const infoDiv = document.getElementById('systemInfoDetails'); infoDiv.innerHTML = ` -
Python${sysInfo.python_version || '-'}
-
OS${sysInfo.os || '-'}
-
Architecture${sysInfo.architecture || '-'}
-
CPU Cores${sysInfo.cpu_count || '-'}
-
Mémoire${sysInfo.memory_total ? Math.round(sysInfo.memory_total / 1024 / 1024 / 1024) + ' GB' : '-'}
-
Démarré le${version.build_date ? new Date(version.build_date).toLocaleString('fr-FR') : '-'}
+
Python${sysDetails.python_version || '-'}
+
Base path${sysDetails.base_path || '-'}
+
Backups disponibles${sysInfo.backups_available || 0}
+
Version${vInfo.version || '-'}
+
Build${vInfo.build || '-'}
+
Date${vInfo.date || '-'}
`; // Check for updates @@ -2360,9 +2421,9 @@
${b.label || 'Point de restauration'}
- ${new Date(b.created_at || b.timestamp).toLocaleString('fr-FR')} + ${new Date(b.created_at || b.timestamp).toLocaleString('fr-FR')} — v${b.version || '-'}
- +
`).join(''); } catch (e) { @@ -2388,15 +2449,8 @@ } async function restoreVersion(backupId) { - if (!confirm('Restaurer ce point ? Les données actuelles seront sauvegardées automatiquement.')) return; - - showNotification('⏮️ Restauration en cours...', 'warning'); - try { - await fetchJSON(`/api/version/rollback/${backupId}`, { method: 'POST' }); - showNotification('✅ Restauration effectuée. Redémarrage recommandé.', 'success'); - } catch (e) { - showNotification(`❌ Erreur: ${e.message}`, 'error'); - } + // La fonctionnalité de rollback n'est pas encore implémentée côté serveur + showNotification('La restauration n\'est pas encore disponible dans cette version.', 'warning'); } async function uploadUpdatePackage() { @@ -2441,19 +2495,24 @@ async function refreshCorrectionPacks() { try { - // Essayer d'abord l'API VWB (port 5000) + // Essayer d'abord l'API VWB (port 5002) let data; try { - data = await fetchJSON('http://localhost:5000/api/correction-packs/stats'); + data = await fetchJSON('http://localhost:5002/api/correction-packs/stats'); } catch (e) { // Fallback sur les stats de backup - const backup = await fetchJSON('/api/backup/stats'); - data = { - total_packs: backup.categories?.correction_packs?.count || 0, - total_corrections: 0, - total_applications: 0, - overall_success_rate: 0 - }; + try { + const backup = await fetchJSON('/api/backup/stats'); + const stats = backup.stats || {}; + data = { + total_packs: stats.correction_packs?.file_count || 0, + total_corrections: 0, + total_applications: 0, + overall_success_rate: 0 + }; + } catch (e2) { + data = { total_packs: 0, total_corrections: 0, total_applications: 0, overall_success_rate: 0 }; + } } document.getElementById('statPacks').textContent = data.total_packs || 0; @@ -2476,7 +2535,7 @@ try { let packs; try { - const data = await fetchJSON('http://localhost:5000/api/correction-packs'); + const data = await fetchJSON('http://localhost:5002/api/correction-packs'); packs = data.packs || []; } catch (e) { packs = []; @@ -2552,7 +2611,7 @@ showNotification('📦 Création du pack...', 'info'); try { - await fetchJSON('http://localhost:5000/api/correction-packs', { + await fetchJSON('http://localhost:5002/api/correction-packs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, description, category }) @@ -2569,7 +2628,7 @@ async function exportPack(packId) { showNotification('⬇️ Export du pack...', 'info'); try { - const response = await fetch(`http://localhost:5000/api/correction-packs/${packId}/export`); + const response = await fetch(`http://localhost:5002/api/correction-packs/${packId}/export`); if (!response.ok) throw new Error(`HTTP ${response.status}`); const blob = await response.blob(); @@ -2593,7 +2652,7 @@ showNotification('🗑️ Suppression...', 'warning'); try { - await fetchJSON(`http://localhost:5000/api/correction-packs/${packId}`, { method: 'DELETE' }); + await fetchJSON(`http://localhost:5002/api/correction-packs/${packId}`, { method: 'DELETE' }); showNotification('✅ Pack supprimé', 'success'); await refreshCorrectionPacks(); } catch (e) { @@ -2612,9 +2671,9 @@ // Stats depuis l'API de performance const perf = await fetchJSON('/api/system/performance'); - // Stats FAISS + // Stats FAISS — la clé est total_vectors ou total_embeddings const faiss = perf.faiss || {}; - document.getElementById('statCorpusSize').textContent = faiss.vectors || 0; + document.getElementById('statCorpusSize').textContent = faiss.total_vectors || faiss.total_embeddings || 0; // Stats sessions const status = await fetchJSON('/api/system/status'); @@ -2624,7 +2683,7 @@ let healingRate = '-'; let learningRate = '-'; try { - const corrections = await fetchJSON('http://localhost:5000/api/correction-packs/stats'); + const corrections = await fetchJSON('http://localhost:5002/api/correction-packs/stats'); healingRate = corrections.overall_success_rate ? Math.round(corrections.overall_success_rate * 100) + '%' : '-'; learningRate = corrections.total_corrections > 0 ? @@ -2650,30 +2709,7 @@ async function loadActionTypeStats() { const div = document.getElementById('actionTypeStats'); - - // Données simulées basées sur les types d'actions disponibles - const actionTypes = [ - { name: 'click', count: 45, color: '#3b82f6' }, - { name: 'type_text', count: 28, color: '#8b5cf6' }, - { name: 'wait_for_anchor', count: 15, color: '#22c55e' }, - { name: 'scroll', count: 8, color: '#f59e0b' }, - { name: 'hotkey', count: 4, color: '#ef4444' } - ]; - - const total = actionTypes.reduce((s, a) => s + a.count, 0); - - div.innerHTML = actionTypes.map(a => { - const pct = total > 0 ? Math.round(a.count / total * 100) : 0; - return ` -
- ${a.name} - ${a.count} (${pct}%) -
-
-
-
- `; - }).join(''); + div.innerHTML = '

Statistiques par type d\'action non encore disponibles.
Cette fonctionnalite sera alimentee par les sessions traitees.

'; } async function loadTopCorrections() { @@ -2682,7 +2718,7 @@ try { let corrections = []; try { - const data = await fetchJSON('http://localhost:5000/api/correction-packs'); + const data = await fetchJSON('http://localhost:5002/api/correction-packs'); // Agrégation des corrections les plus utilisées for (const pack of (data.packs || [])) { if (pack.applications_count > 0) { @@ -2850,12 +2886,12 @@ services: { vwb_backend: { host: document.getElementById('cfg_vwb_backend_host').value || 'localhost', - port: parseInt(document.getElementById('cfg_vwb_backend_port').value) || 5000, + port: parseInt(document.getElementById('cfg_vwb_backend_port').value) || 5002, description: 'Visual Workflow Builder - Backend API' }, vwb_frontend: { host: document.getElementById('cfg_vwb_frontend_host').value || 'localhost', - port: parseInt(document.getElementById('cfg_vwb_frontend_port').value) || 3000, + port: parseInt(document.getElementById('cfg_vwb_frontend_port').value) || 3002, description: 'Visual Workflow Builder - Interface React' }, web_dashboard: { @@ -2865,9 +2901,14 @@ }, agent_chat: { host: document.getElementById('cfg_agent_chat_host').value || 'localhost', - port: parseInt(document.getElementById('cfg_agent_chat_port').value) || 5002, + port: parseInt(document.getElementById('cfg_agent_chat_port').value) || 5004, description: 'Agent conversationnel RPA' }, + streaming: { + host: document.getElementById('cfg_streaming_host').value || 'localhost', + port: parseInt(document.getElementById('cfg_streaming_port').value) || 5005, + description: 'Serveur de streaming et capture temps réel' + }, upload_api: { host: document.getElementById('cfg_upload_api_host').value || 'localhost', port: parseInt(document.getElementById('cfg_upload_api_port').value) || 8000, @@ -3128,6 +3169,183 @@ input.value = ''; // Reset file input } + // ============================================================ + // SECTION: Streaming + // ============================================================ + + // Utiliser le proxy du dashboard pour éviter les problèmes CORS + const STREAMING_BASE = '/api/streaming'; + const VWB_IMPORT_URL = 'http://localhost:5002/api/workflows/import-core'; + + async function refreshStreaming() { + await Promise.all([ + refreshStreamingStats(), + refreshStreamingSessions(), + refreshStreamingWorkflows() + ]); + } + + async function refreshStreamingStats() { + const statusEl = document.getElementById('streamServerStatus'); + const detailsEl = document.getElementById('streamServerDetails'); + + try { + const data = await fetchJSON(`${STREAMING_BASE}/stats`); + + statusEl.innerHTML = ''; + statusEl.title = 'Serveur streaming en ligne'; + + document.getElementById('streamActiveSessions').textContent = data.active_sessions || 0; + document.getElementById('streamTotalEvents').textContent = data.total_events || 0; + document.getElementById('streamWorkflowsBuilt').textContent = data.workflows_built || 0; + + // Détails du serveur + const rows = []; + if (data.uptime !== undefined) rows.push({label: 'Uptime', value: formatUptime(data.uptime)}); + if (data.total_sessions !== undefined) rows.push({label: 'Sessions totales', value: data.total_sessions}); + if (data.active_sessions !== undefined) rows.push({label: 'Sessions actives', value: data.active_sessions}); + if (data.total_events !== undefined) rows.push({label: 'Événements totaux', value: data.total_events}); + if (data.workflows_built !== undefined) rows.push({label: 'Workflows construits', value: data.workflows_built}); + if (data.events_per_second !== undefined) rows.push({label: 'Événements/sec', value: (data.events_per_second || 0).toFixed(2)}); + if (data.memory_usage_mb !== undefined) rows.push({label: 'Mémoire utilisée', value: Math.round(data.memory_usage_mb) + ' MB'}); + if (data.server_version) rows.push({label: 'Version serveur', value: data.server_version}); + + if (rows.length === 0) { + // Afficher les données brutes si les clés attendues ne sont pas présentes + const rawRows = Object.entries(data).map(([k, v]) => ({label: k, value: JSON.stringify(v)})); + detailsEl.innerHTML = rawRows.map(r => ` +
${r.label}${r.value}
+ `).join(''); + } else { + detailsEl.innerHTML = rows.map(r => ` +
${r.label}${r.value}
+ `).join(''); + } + + } catch (e) { + statusEl.innerHTML = ''; + statusEl.title = 'Serveur streaming hors ligne'; + document.getElementById('streamActiveSessions').textContent = '-'; + document.getElementById('streamTotalEvents').textContent = '-'; + document.getElementById('streamWorkflowsBuilt').textContent = '-'; + detailsEl.innerHTML = `
+ ❌ Serveur streaming inaccessible (port 5005)
+ ${e.message} +
`; + } + } + + async function refreshStreamingSessions() { + const list = document.getElementById('streamSessionsList'); + + try { + const data = await fetchJSON(`${STREAMING_BASE}/sessions`); + const sessions = data.sessions || data || []; + + if (!Array.isArray(sessions) || sessions.length === 0) { + list.innerHTML = '

Aucune session de streaming active

'; + return; + } + + list.innerHTML = sessions.map(s => { + const sessionId = s.session_id || s.id || 'N/A'; + const status = s.status || 'inconnu'; + const events = s.events_count || s.total_events || 0; + const started = s.started_at ? new Date(s.started_at).toLocaleString('fr-FR') : '-'; + const statusColor = status === 'active' ? '#22c55e' : (status === 'completed' ? '#3b82f6' : '#64748b'); + + return ` +
+
+

+ ${sessionId} + + ${status} + +

+
📅 ${started} • 🎬 ${events} événements
+
+
+ `; + }).join(''); + + } catch (e) { + list.innerHTML = `

Erreur: ${e.message}

`; + } + } + + async function refreshStreamingWorkflows() { + const list = document.getElementById('streamWorkflowsList'); + + try { + const data = await fetchJSON(`${STREAMING_BASE}/workflows`); + const workflows = data.workflows || data || []; + + if (!Array.isArray(workflows) || workflows.length === 0) { + list.innerHTML = '

Aucun workflow construit par le streaming

'; + return; + } + + list.innerHTML = workflows.map(wf => { + const wfId = wf.workflow_id || wf.id || 'N/A'; + const name = wf.name || wfId; + const nodes = wf.nodes_count || wf.nodes || 0; + const edges = wf.edges_count || wf.edges || 0; + const created = wf.created_at ? new Date(wf.created_at).toLocaleString('fr-FR') : '-'; + const sessionId = wf.source_session_id || wf.session_id || '-'; + + return ` +
+
+

${name}

+
📅 ${created} • 📦 ${nodes} nœuds, ${edges} arêtes • 🎬 Session: ${sessionId}
+
+
+ +
+
+ `; + }).join(''); + + } catch (e) { + list.innerHTML = `

Erreur: ${e.message}

`; + } + } + + async function importStreamingWorkflow(workflowId) { + if (!confirm(`Importer le workflow "${workflowId}" dans le Visual Workflow Builder ?`)) return; + + showNotification('📥 Import en cours...', 'info'); + + try { + const data = await fetchJSON(VWB_IMPORT_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ workflow_id: workflowId, source: 'streaming' }) + }); + + if (data.error) { + showNotification('❌ Erreur: ' + data.error, 'error'); + } else { + showNotification('✅ Workflow importé dans le VWB', 'success'); + } + } catch (e) { + showNotification('❌ Erreur: ' + e.message, 'error'); + } + } + + function formatUptime(seconds) { + if (!seconds && seconds !== 0) return '-'; + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = Math.floor(seconds % 60); + if (h > 0) return `${h}h ${m}m`; + if (m > 0) return `${m}m ${s}s`; + return `${s}s`; + } + // Initialize document.addEventListener('DOMContentLoaded', () => { initOverviewChart(); @@ -3135,12 +3353,12 @@ refreshSystemStatus(); refreshWorkflows(); setInterval(refreshSystemStatus, 10000); - setInterval(refreshServices, 5000); // Rafraîchir les services toutes les 5s + setInterval(refreshServices, 15000); // Rafraîchir les services toutes les 15s - // Charger les stats des nouvelles sections (en arrière-plan) + // Charger les stats des sections secondaires (en arrière-plan) setTimeout(() => { refreshBackupStats(); - refreshSystemInfo(); + refreshSystemInfo(); // Chargé dans l'onglet Services }, 1000); });