fix(dashboard): corriger les routes mortes, parsing API et liens cassés
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 <noreply@anthropic.com>
This commit is contained in:
171
tests/unit/test_dashboard_routes.py
Normal file
171
tests/unit/test_dashboard_routes.py
Normal file
@@ -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
|
||||
@@ -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/<path:endpoint>')
|
||||
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
|
||||
# =============================================================================
|
||||
|
||||
@@ -53,15 +53,12 @@
|
||||
<div class="tab" onclick="switchTab('overview')">📊 Vue d'ensemble</div>
|
||||
<div class="tab" onclick="switchTab('execution')">⚡ Exécution</div>
|
||||
<div class="tab" onclick="switchTab('workflows')">🔄 Workflows</div>
|
||||
<div class="tab" onclick="switchTab('chains')">🔗 Chaînes</div>
|
||||
<div class="tab" onclick="switchTab('triggers')">⚡ Déclencheurs</div>
|
||||
<div class="tab" onclick="switchTab('sessions')">📦 Sessions</div>
|
||||
<div class="tab" onclick="switchTab('performance')">📈 Performance</div>
|
||||
<div class="tab" onclick="switchTab('metrics')">📊 Métriques</div>
|
||||
<div class="tab" onclick="switchTab('streaming')">📡 Streaming</div>
|
||||
<div class="tab" onclick="switchTab('logs')">📄 Logs</div>
|
||||
<div class="tab" onclick="switchTab('tests')">🧪 Tests</div>
|
||||
<div class="tab" onclick="switchTab('backups')">💾 Sauvegardes</div>
|
||||
<div class="tab" onclick="switchTab('system')">⚙️ Système</div>
|
||||
<div class="tab" onclick="switchTab('corrections')">🔧 Corrections</div>
|
||||
<div class="tab" onclick="switchTab('learning')">🧠 Apprentissage</div>
|
||||
<div class="tab" onclick="switchTab('config')">🔧 Configuration</div>
|
||||
@@ -95,6 +92,62 @@
|
||||
<!-- Généré dynamiquement -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Système & Version (fusionné depuis l'ancien onglet Système) -->
|
||||
<div class="card" style="margin-top:20px;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:15px;">
|
||||
<h2 style="margin-bottom:0;"><span class="icon">⚙️</span> Système & Version</h2>
|
||||
<button class="btn btn-primary btn-small" onclick="refreshSystemInfo()">🔄 Actualiser</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-3" style="margin-top:15px;">
|
||||
<div class="card stat-card">
|
||||
<div class="stat-value" id="sysVersion">-</div>
|
||||
<div class="stat-label">Version</div>
|
||||
</div>
|
||||
<div class="card stat-card">
|
||||
<div class="stat-value" id="sysCommit">-</div>
|
||||
<div class="stat-label">Commit</div>
|
||||
</div>
|
||||
<div class="card stat-card">
|
||||
<div id="sysUpdateStatus" style="font-size:24px;">⏳</div>
|
||||
<div class="stat-label">Mise à jour</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-2" style="margin-top:15px;">
|
||||
<div class="card">
|
||||
<h2><span class="icon">🖥️</span> Informations système</h2>
|
||||
<div class="system-info" id="systemInfoDetails">
|
||||
<div class="loading"><div class="spinner"></div>Chargement...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2><span class="icon">⏮️</span> Points de restauration</h2>
|
||||
<div class="actions-bar">
|
||||
<button class="btn btn-success btn-small" onclick="createVersionBackup()">➕ Créer un point</button>
|
||||
</div>
|
||||
<div class="backup-list" id="versionBackupsList">
|
||||
<div class="loading"><div class="spinner"></div>Chargement...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:15px;">
|
||||
<h2><span class="icon">📤</span> Mise à jour manuelle</h2>
|
||||
<div style="display:flex;gap:20px;align-items:center;flex-wrap:wrap;">
|
||||
<div style="flex:1;min-width:300px;">
|
||||
<p style="color:#94a3b8;margin-bottom:15px;">Uploadez un package de mise à jour (.zip) contenant un manifest valide</p>
|
||||
<input type="file" id="updateFileInput" accept=".zip" style="display:none;" onchange="uploadUpdatePackage()">
|
||||
<button class="btn btn-warning" onclick="document.getElementById('updateFileInput').click()">
|
||||
📤 Uploader un package
|
||||
</button>
|
||||
</div>
|
||||
<div id="updatePackageInfo" style="flex:1;min-width:300px;padding:15px;background:#0f172a;border-radius:8px;display:none;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Vue d'ensemble -->
|
||||
@@ -194,6 +247,30 @@
|
||||
<div class="loading"><div class="spinner"></div>Chargement...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sous-section : Chaînes de Workflows -->
|
||||
<div class="card" style="margin-top:20px;">
|
||||
<h2><span class="icon">🔗</span> Chaînes de Workflows</h2>
|
||||
<div class="actions-bar">
|
||||
<button class="btn btn-primary btn-small" onclick="refreshChains()">🔄 Actualiser</button>
|
||||
<button class="btn btn-success btn-small" onclick="showCreateChainModal()">➕ Nouvelle Chaîne</button>
|
||||
</div>
|
||||
<div class="workflow-grid" id="chainList">
|
||||
<div class="loading"><div class="spinner"></div>Chargement...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sous-section : Déclencheurs -->
|
||||
<div class="card" style="margin-top:20px;">
|
||||
<h2><span class="icon">⚡</span> Déclencheurs</h2>
|
||||
<div class="actions-bar">
|
||||
<button class="btn btn-primary btn-small" onclick="refreshTriggers()">🔄 Actualiser</button>
|
||||
<button class="btn btn-success btn-small" onclick="showCreateTriggerModal()">➕ Nouveau Déclencheur</button>
|
||||
</div>
|
||||
<div class="workflow-grid" id="triggerList">
|
||||
<div class="loading"><div class="spinner"></div>Chargement...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Sessions -->
|
||||
@@ -293,39 +370,9 @@
|
||||
<canvas id="cacheChart" height="250"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Chaînes -->
|
||||
<div id="tab-chains" class="tab-content">
|
||||
<div class="card">
|
||||
<h2><span class="icon">🔗</span> Chaînes de Workflows</h2>
|
||||
<div class="actions-bar">
|
||||
<button class="btn btn-primary btn-small" onclick="refreshChains()">🔄 Actualiser</button>
|
||||
<button class="btn btn-success btn-small" onclick="showCreateChainModal()">➕ Nouvelle Chaîne</button>
|
||||
</div>
|
||||
<div class="workflow-grid" id="chainList">
|
||||
<div class="loading"><div class="spinner"></div>Chargement...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Triggers -->
|
||||
<div id="tab-triggers" class="tab-content">
|
||||
<div class="card">
|
||||
<h2><span class="icon">⚡</span> Déclencheurs</h2>
|
||||
<div class="actions-bar">
|
||||
<button class="btn btn-primary btn-small" onclick="refreshTriggers()">🔄 Actualiser</button>
|
||||
<button class="btn btn-success btn-small" onclick="showCreateTriggerModal()">➕ Nouveau Déclencheur</button>
|
||||
</div>
|
||||
<div class="workflow-grid" id="triggerList">
|
||||
<div class="loading"><div class="spinner"></div>Chargement...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Métriques Prometheus -->
|
||||
<div id="tab-metrics" class="tab-content">
|
||||
<div class="grid grid-4">
|
||||
|
||||
<!-- Métriques Prometheus (fusionné depuis l'ancien onglet Métriques) -->
|
||||
<div class="grid grid-4" style="margin-top:20px;">
|
||||
<div class="card stat-card">
|
||||
<div class="stat-value" id="metricWorkflows">0</div>
|
||||
<div class="stat-label">Workflows Exécutés</div>
|
||||
@@ -343,8 +390,8 @@
|
||||
<div class="stat-label">Taux d'Erreur</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
|
||||
<div class="card" style="margin-top:20px;">
|
||||
<h2><span class="icon">🤖</span> Automatisation</h2>
|
||||
<div class="actions-bar">
|
||||
<button class="btn btn-primary btn-small" onclick="refreshAutomationStatus()">🔄 Actualiser</button>
|
||||
@@ -356,8 +403,8 @@
|
||||
<div class="stat-row"><span>Intervalle:</span><span id="autoInterval">-</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
|
||||
<div class="card" style="margin-top:20px;">
|
||||
<h2><span class="icon">📊</span> Métriques Prometheus</h2>
|
||||
<div class="actions-bar">
|
||||
<button class="btn btn-primary btn-small" onclick="refreshMetrics()">🔄 Actualiser</button>
|
||||
@@ -367,6 +414,67 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Streaming -->
|
||||
<div id="tab-streaming" class="tab-content">
|
||||
<div class="card" style="margin-bottom:20px;background:linear-gradient(135deg, #1e293b 0%, #0f172a 100%);">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:15px;">
|
||||
<div>
|
||||
<h2 style="margin-bottom:5px;color:#e2e8f0;"><span class="icon">📡</span> Streaming & Capture Temps Réel</h2>
|
||||
<p style="color:#64748b;font-size:13px;">Suivi des sessions de streaming, workflows générés et statistiques du serveur de capture</p>
|
||||
</div>
|
||||
<div style="display:flex;gap:10px;">
|
||||
<button class="btn btn-primary" onclick="refreshStreaming()">🔄 Actualiser</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats du serveur streaming -->
|
||||
<div class="grid grid-4">
|
||||
<div class="card stat-card">
|
||||
<div class="stat-value" id="streamActiveSessions">-</div>
|
||||
<div class="stat-label">Sessions actives</div>
|
||||
</div>
|
||||
<div class="card stat-card">
|
||||
<div class="stat-value" id="streamTotalEvents">-</div>
|
||||
<div class="stat-label">Événements totaux</div>
|
||||
</div>
|
||||
<div class="card stat-card">
|
||||
<div class="stat-value" id="streamWorkflowsBuilt">-</div>
|
||||
<div class="stat-label">Workflows construits</div>
|
||||
</div>
|
||||
<div class="card stat-card">
|
||||
<div id="streamServerStatus" style="font-size:24px;">⏳</div>
|
||||
<div class="stat-label">Serveur streaming</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-2" style="margin-top:20px;">
|
||||
<!-- Sessions de streaming actives -->
|
||||
<div class="card">
|
||||
<h2><span class="icon">🎬</span> Sessions de streaming actives</h2>
|
||||
<div id="streamSessionsList" class="session-list" style="max-height:400px;">
|
||||
<div class="loading"><div class="spinner"></div>Chargement...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Workflows construits par le streaming -->
|
||||
<div class="card">
|
||||
<h2><span class="icon">🔄</span> Workflows construits par le streaming</h2>
|
||||
<div id="streamWorkflowsList" class="session-list" style="max-height:400px;">
|
||||
<div class="loading"><div class="spinner"></div>Chargement...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Détails du serveur -->
|
||||
<div class="card" style="margin-top:20px;">
|
||||
<h2><span class="icon">📊</span> Statistiques détaillées du serveur</h2>
|
||||
<div class="live-stats" id="streamServerDetails">
|
||||
<div class="loading"><div class="spinner"></div>Chargement...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Logs -->
|
||||
<div id="tab-logs" class="tab-content">
|
||||
<div class="card">
|
||||
@@ -483,67 +591,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Système -->
|
||||
<div id="tab-system" class="tab-content">
|
||||
<div class="card" style="margin-bottom:20px;background:linear-gradient(135deg, #1e293b 0%, #0f172a 100%);">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:15px;">
|
||||
<div>
|
||||
<h2 style="margin-bottom:5px;color:#e2e8f0;"><span class="icon">⚙️</span> Système & Version</h2>
|
||||
<p style="color:#64748b;font-size:13px;">Informations système, mises à jour et points de restauration</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="refreshSystemInfo()">🔄 Actualiser</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-3">
|
||||
<div class="card stat-card">
|
||||
<div class="stat-value" id="sysVersion">-</div>
|
||||
<div class="stat-label">Version</div>
|
||||
</div>
|
||||
<div class="card stat-card">
|
||||
<div class="stat-value" id="sysCommit">-</div>
|
||||
<div class="stat-label">Commit</div>
|
||||
</div>
|
||||
<div class="card stat-card">
|
||||
<div id="sysUpdateStatus" style="font-size:24px;">⏳</div>
|
||||
<div class="stat-label">Mise à jour</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<h2><span class="icon">🖥️</span> Informations système</h2>
|
||||
<div class="system-info" id="systemInfoDetails">
|
||||
<div class="loading"><div class="spinner"></div>Chargement...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2><span class="icon">⏮️</span> Points de restauration</h2>
|
||||
<div class="actions-bar">
|
||||
<button class="btn btn-success btn-small" onclick="createVersionBackup()">➕ Créer un point</button>
|
||||
</div>
|
||||
<div class="backup-list" id="versionBackupsList">
|
||||
<div class="loading"><div class="spinner"></div>Chargement...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:20px;">
|
||||
<h2><span class="icon">📤</span> Mise à jour manuelle</h2>
|
||||
<div style="display:flex;gap:20px;align-items:center;flex-wrap:wrap;">
|
||||
<div style="flex:1;min-width:300px;">
|
||||
<p style="color:#94a3b8;margin-bottom:15px;">Uploadez un package de mise à jour (.zip) contenant un manifest valide</p>
|
||||
<input type="file" id="updateFileInput" accept=".zip" style="display:none;" onchange="uploadUpdatePackage()">
|
||||
<button class="btn btn-warning" onclick="document.getElementById('updateFileInput').click()">
|
||||
📤 Uploader un package
|
||||
</button>
|
||||
</div>
|
||||
<div id="updatePackageInfo" style="flex:1;min-width:300px;padding:15px;background:#0f172a;border-radius:8px;display:none;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Correction Packs -->
|
||||
<div id="tab-corrections" class="tab-content">
|
||||
<div class="card" style="margin-bottom:20px;background:linear-gradient(135deg, #1e293b 0%, #0f172a 100%);">
|
||||
@@ -690,31 +737,39 @@
|
||||
<h2><span class="icon">🌐</span> Services & Ports</h2>
|
||||
<div id="configServices" class="config-section">
|
||||
<div class="config-item">
|
||||
<label>VWB Backend</label>
|
||||
<label>VWB Backend (port 5002)</label>
|
||||
<div style="display:flex;gap:10px;">
|
||||
<input type="text" id="cfg_vwb_backend_host" placeholder="localhost" class="config-input" style="flex:2;">
|
||||
<input type="number" id="cfg_vwb_backend_port" placeholder="5000" class="config-input" style="flex:1;">
|
||||
<input type="number" id="cfg_vwb_backend_port" placeholder="5002" class="config-input" style="flex:1;">
|
||||
<button class="btn btn-small btn-primary" onclick="testConnection('service', 'vwb_backend')">Test</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>VWB Frontend</label>
|
||||
<label>VWB Frontend (port 3002)</label>
|
||||
<div style="display:flex;gap:10px;">
|
||||
<input type="text" id="cfg_vwb_frontend_host" placeholder="localhost" class="config-input" style="flex:2;">
|
||||
<input type="number" id="cfg_vwb_frontend_port" placeholder="3000" class="config-input" style="flex:1;">
|
||||
<input type="number" id="cfg_vwb_frontend_port" placeholder="3002" class="config-input" style="flex:1;">
|
||||
<button class="btn btn-small btn-primary" onclick="testConnection('service', 'vwb_frontend')">Test</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Agent Chat</label>
|
||||
<label>Agent Chat (port 5004)</label>
|
||||
<div style="display:flex;gap:10px;">
|
||||
<input type="text" id="cfg_agent_chat_host" placeholder="localhost" class="config-input" style="flex:2;">
|
||||
<input type="number" id="cfg_agent_chat_port" placeholder="5002" class="config-input" style="flex:1;">
|
||||
<input type="number" id="cfg_agent_chat_port" placeholder="5004" class="config-input" style="flex:1;">
|
||||
<button class="btn btn-small btn-primary" onclick="testConnection('service', 'agent_chat')">Test</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>API Upload</label>
|
||||
<label>Streaming (port 5005)</label>
|
||||
<div style="display:flex;gap:10px;">
|
||||
<input type="text" id="cfg_streaming_host" placeholder="localhost" class="config-input" style="flex:2;">
|
||||
<input type="number" id="cfg_streaming_port" placeholder="5005" class="config-input" style="flex:1;">
|
||||
<button class="btn btn-small btn-primary" onclick="testConnection('service', 'streaming')">Test</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>API Upload (port 8000)</label>
|
||||
<div style="display:flex;gap:10px;">
|
||||
<input type="text" id="cfg_upload_api_host" placeholder="localhost" class="config-input" style="flex:2;">
|
||||
<input type="number" id="cfg_upload_api_port" placeholder="8000" class="config-input" style="flex:1;">
|
||||
@@ -859,7 +914,7 @@
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Origines autorisees (CORS)</label>
|
||||
<input type="text" id="cfg_security_origins" placeholder="http://localhost:3000,http://localhost:5001" class="config-input">
|
||||
<input type="text" id="cfg_security_origins" placeholder="http://localhost:3002,http://localhost:5001,http://localhost:5002" class="config-input">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -894,7 +949,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Resultat des tests -->
|
||||
<div class="card" style="margin-top:20px;" id="configTestResults" style="display:none;">
|
||||
<div class="card" style="margin-top:20px;" id="configTestResults">
|
||||
<h2><span class="icon">✅</span> Resultats des tests</h2>
|
||||
<div id="testResultsContent" style="padding:15px;background:#0f172a;border-radius:8px;">
|
||||
<p style="color:#64748b;text-align:center;">Cliquez sur "Test" pour verifier les connexions</p>
|
||||
@@ -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 = `
|
||||
<div class="system-info-row"><span>Python</span><span>${sysInfo.python_version || '-'}</span></div>
|
||||
<div class="system-info-row"><span>OS</span><span>${sysInfo.os || '-'}</span></div>
|
||||
<div class="system-info-row"><span>Architecture</span><span>${sysInfo.architecture || '-'}</span></div>
|
||||
<div class="system-info-row"><span>CPU Cores</span><span>${sysInfo.cpu_count || '-'}</span></div>
|
||||
<div class="system-info-row"><span>Mémoire</span><span>${sysInfo.memory_total ? Math.round(sysInfo.memory_total / 1024 / 1024 / 1024) + ' GB' : '-'}</span></div>
|
||||
<div class="system-info-row"><span>Démarré le</span><span>${version.build_date ? new Date(version.build_date).toLocaleString('fr-FR') : '-'}</span></div>
|
||||
<div class="system-info-row"><span>Python</span><span>${sysDetails.python_version || '-'}</span></div>
|
||||
<div class="system-info-row"><span>Base path</span><span>${sysDetails.base_path || '-'}</span></div>
|
||||
<div class="system-info-row"><span>Backups disponibles</span><span>${sysInfo.backups_available || 0}</span></div>
|
||||
<div class="system-info-row"><span>Version</span><span>${vInfo.version || '-'}</span></div>
|
||||
<div class="system-info-row"><span>Build</span><span>${vInfo.build || '-'}</span></div>
|
||||
<div class="system-info-row"><span>Date</span><span>${vInfo.date || '-'}</span></div>
|
||||
`;
|
||||
|
||||
// Check for updates
|
||||
@@ -2360,9 +2421,9 @@
|
||||
<div class="version-backup-item">
|
||||
<div class="version-backup-info">
|
||||
<h5>${b.label || 'Point de restauration'}</h5>
|
||||
<span>${new Date(b.created_at || b.timestamp).toLocaleString('fr-FR')}</span>
|
||||
<span>${new Date(b.created_at || b.timestamp).toLocaleString('fr-FR')} — v${b.version || '-'}</span>
|
||||
</div>
|
||||
<button class="btn btn-warning btn-small" onclick="restoreVersion('${b.id || b.path}')">⏮️ Restaurer</button>
|
||||
<button class="btn btn-secondary btn-small" onclick="restoreVersion('${b.id || b.path}')" title="Restauration non disponible dans cette version" disabled style="opacity:0.5;cursor:not-allowed;">⏮️ Restaurer</button>
|
||||
</div>
|
||||
`).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 `
|
||||
<div class="stat-bar">
|
||||
<span class="stat-bar-label">${a.name}</span>
|
||||
<span class="stat-bar-value">${a.count} (${pct}%)</span>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width:${pct}%;background:${a.color};"></div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
div.innerHTML = '<p style="color:#64748b;text-align:center;padding:20px;">Statistiques par type d\'action non encore disponibles.<br><span style="font-size:12px;">Cette fonctionnalite sera alimentee par les sessions traitees.</span></p>';
|
||||
}
|
||||
|
||||
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 = '<span style="color:#22c55e;">✅</span>';
|
||||
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 => `
|
||||
<div class="stat-row"><span>${r.label}</span><span>${r.value}</span></div>
|
||||
`).join('');
|
||||
} else {
|
||||
detailsEl.innerHTML = rows.map(r => `
|
||||
<div class="stat-row"><span>${r.label}</span><span>${r.value}</span></div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
statusEl.innerHTML = '<span style="color:#ef4444;">❌</span>';
|
||||
statusEl.title = 'Serveur streaming hors ligne';
|
||||
document.getElementById('streamActiveSessions').textContent = '-';
|
||||
document.getElementById('streamTotalEvents').textContent = '-';
|
||||
document.getElementById('streamWorkflowsBuilt').textContent = '-';
|
||||
detailsEl.innerHTML = `<div style="text-align:center;padding:20px;color:#ef4444;">
|
||||
❌ Serveur streaming inaccessible (port 5005)<br>
|
||||
<span style="font-size:12px;color:#94a3b8;margin-top:5px;display:block;">${e.message}</span>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
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 = '<p style="text-align:center;color:#64748b;padding:30px;">Aucune session de streaming active</p>';
|
||||
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 `
|
||||
<div class="session-item">
|
||||
<div class="session-info">
|
||||
<h4>
|
||||
${sessionId}
|
||||
<span style="margin-left:10px;padding:3px 8px;border-radius:4px;font-size:12px;background:rgba(${statusColor === '#22c55e' ? '34,197,94' : '59,130,246'},0.15);color:${statusColor};">
|
||||
${status}
|
||||
</span>
|
||||
</h4>
|
||||
<div class="meta">📅 ${started} • 🎬 ${events} événements</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
} catch (e) {
|
||||
list.innerHTML = `<p style="color:#ef4444;text-align:center;padding:20px;">Erreur: ${e.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
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 = '<p style="text-align:center;color:#64748b;padding:30px;">Aucun workflow construit par le streaming</p>';
|
||||
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 `
|
||||
<div class="session-item">
|
||||
<div class="session-info">
|
||||
<h4>${name}</h4>
|
||||
<div class="meta">📅 ${created} • 📦 ${nodes} nœuds, ${edges} arêtes • 🎬 Session: ${sessionId}</div>
|
||||
</div>
|
||||
<div class="session-actions">
|
||||
<button class="btn btn-success btn-small" onclick="importStreamingWorkflow('${wfId}')">
|
||||
📥 Importer dans VWB
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
} catch (e) {
|
||||
list.innerHTML = `<p style="color:#ef4444;text-align:center;padding:20px;">Erreur: ${e.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user