Files
rpa_vision_v3/web_dashboard/templates/index.html
Dom bb4ed2a75d feat(dashboard): session cleaner intégré + auth + nettoyage UI
- Onglet "🧹 Nettoyage" dans le dashboard (iframe vers port 5006)
- Indicateur d'état + bouton de démarrage si cleaner down
- Service systemd rpa-session-cleaner intégré au target rpa-vision
- svc.sh et services.conf incluent session-cleaner (port 5006)

P0-A — Auth dashboard Flask :
- HTTP Basic obligatoire sur tous les endpoints (sauf /health, /healthz)
- Credentials via DASHBOARD_USER + DASHBOARD_PASSWORD
- 13 tests

Nettoyage UI :
- Section "Détection Visuelle" OWL retirée (modèle remplacé par pipeline VLM)
- Dashboard préfère auto shot_*_blurred.png (avec ?raw=1 pour brut)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:48:36 +02:00

3456 lines
175 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RPA Vision V3 - Dashboard</title>
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; }
.header { background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); color: white; padding: 20px 30px; display: flex; justify-content: space-between; align-items: center; }
.header h1 { font-size: 24px; display: flex; align-items: center; gap: 10px; }
.header .status { display: flex; align-items: center; gap: 8px; font-size: 14px; }
.status-dot { width: 10px; height: 10px; border-radius: 50%; background: #22c55e; animation: pulse 2s infinite; }
.status-dot.offline { background: #ef4444; animation: none; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
.tabs { background: #1e293b; border-bottom: 1px solid #334155; display: flex; padding: 0 20px; }
.tab { padding: 15px 25px; cursor: pointer; border-bottom: 3px solid transparent; transition: all 0.2s; color: #94a3b8; font-weight: 500; }
.tab:hover { color: #e2e8f0; background: #334155; }
.tab.active { border-bottom-color: #3b82f6; color: #3b82f6; }
.container { max-width: 1600px; margin: 0 auto; padding: 20px; }
.tab-content { display: none; }
.tab-content.active { display: block; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 20px; }
.grid-2 { grid-template-columns: 1fr 1fr; }
.grid-3 { grid-template-columns: 1fr 1fr 1fr; }
.grid-4 { grid-template-columns: repeat(4, 1fr); }
.card { background: #1e293b; border-radius: 12px; padding: 20px; border: 1px solid #334155; }
.card h2 { font-size: 16px; margin-bottom: 15px; color: #94a3b8; display: flex; align-items: center; gap: 8px; }
.card h2 .icon { font-size: 20px; }
.stat-card { text-align: center; }
.stat-value { font-size: 36px; font-weight: bold; color: #3b82f6; }
.stat-label { font-size: 12px; color: #64748b; margin-top: 5px; text-transform: uppercase; }
</style>
</head>
<body>
<div class="header">
<h1>🚀 RPA Vision V3 Dashboard</h1>
<div style="display:flex;align-items:center;gap:20px;">
<nav style="display:flex;gap:8px;">
<a href="/gestures" style="color:rgba(255,255,255,0.8);text-decoration:none;font-size:13px;padding:6px 14px;border-radius:6px;transition:all 0.2s;background:rgba(255,255,255,0.1);" onmouseover="this.style.background='rgba(255,255,255,0.2)'" onmouseout="this.style.background='rgba(255,255,255,0.1)'">🤲 Gestes</a>
<a href="/streaming" style="color:rgba(255,255,255,0.8);text-decoration:none;font-size:13px;padding:6px 14px;border-radius:6px;transition:all 0.2s;background:rgba(255,255,255,0.1);" onmouseover="this.style.background='rgba(255,255,255,0.2)'" onmouseout="this.style.background='rgba(255,255,255,0.1)'">📡 Streaming</a>
<a href="/extractions" style="color:rgba(255,255,255,0.8);text-decoration:none;font-size:13px;padding:6px 14px;border-radius:6px;transition:all 0.2s;background:rgba(255,255,255,0.1);" onmouseover="this.style.background='rgba(255,255,255,0.2)'" onmouseout="this.style.background='rgba(255,255,255,0.1)'">📊 Extractions</a>
</nav>
<div class="status">
<div class="status-dot" id="statusDot"></div>
<span id="statusText">Connecté</span>
</div>
</div>
</div>
<div class="tabs">
<div class="tab active" onclick="switchTab('services')">🎛️ Services</div>
<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('sessions')">📦 Sessions</div>
<div class="tab" onclick="switchTab('performance')">📈 Performance</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('corrections')">🔧 Corrections</div>
<div class="tab" onclick="switchTab('learning')">🧠 Apprentissage</div>
<div class="tab" onclick="switchTab('config')">🔧 Configuration</div>
<div class="tab" onclick="switchTab('cleaner')">🧹 Nettoyage</div>
</div>
<div class="container">
<!-- Tab: Services (Panneau de contrôle pour démos) -->
<div id="tab-services" class="tab-content active">
<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> Panneau de Contrôle des Services</h2>
<p style="color:#64748b;font-size:13px;">Gérez tous les services RPA Vision V3 depuis cette interface</p>
</div>
<div style="display:flex;gap:10px;">
<button class="btn btn-success" onclick="startAllServices()">▶️ Tout Démarrer</button>
<button class="btn btn-danger" onclick="stopAllServices()">⏹️ Tout Arrêter</button>
<button class="btn btn-primary" onclick="refreshServices()">🔄 Actualiser</button>
</div>
</div>
</div>
<div class="grid grid-2" id="servicesGrid">
<div class="loading"><div class="spinner"></div>Chargement des services...</div>
</div>
<!-- Liens rapides -->
<div class="card" style="margin-top:20px;">
<h2><span class="icon">🔗</span> Ouvrir dans le Navigateur</h2>
<div style="display:flex;flex-wrap:wrap;gap:15px;margin-top:15px;" id="quickLinks">
<!-- 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 -->
<div id="tab-overview" class="tab-content">
<div class="grid grid-4">
<div class="card stat-card">
<div class="stat-value" id="statSessions">0</div>
<div class="stat-label">Sessions</div>
</div>
<div class="card stat-card">
<div class="stat-value" id="statWorkflows">0</div>
<div class="stat-label">Workflows</div>
</div>
<div class="card stat-card">
<div class="stat-value" id="statTests">0</div>
<div class="stat-label">Tests</div>
</div>
<div class="card stat-card">
<div class="stat-value" id="statExecution">-</div>
<div class="stat-label">Exécution</div>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<h2><span class="icon"></span> État d'exécution temps réel</h2>
<div id="executionStatus" class="execution-panel">
<div class="exec-idle">Aucune exécution en cours</div>
</div>
</div>
<div class="card">
<h2><span class="icon">📈</span> Performance récente</h2>
<canvas id="perfChart" height="200"></canvas>
</div>
</div>
</div>
<!-- Tab: Exécution temps réel -->
<div id="tab-execution" class="tab-content">
<div class="grid grid-2">
<div class="card">
<h2><span class="icon">🎮</span> Contrôle d'exécution</h2>
<div class="exec-controls">
<select id="workflowSelect" class="select-input">
<option value="">Sélectionner un workflow...</option>
</select>
<select id="modeSelect" class="select-input">
<option value="observation">👁️ Observation</option>
<option value="coaching">🎓 Coaching</option>
<option value="supervised" selected>✅ Supervisé</option>
<option value="automatic">🤖 Automatique</option>
</select>
<div class="btn-group">
<button class="btn btn-success" onclick="startExecution()">▶️ Démarrer</button>
<button class="btn btn-warning" onclick="pauseExecution()">⏸️ Pause</button>
<button class="btn btn-danger" onclick="stopExecution()">⏹️ Arrêter</button>
</div>
</div>
</div>
<div class="card">
<h2><span class="icon">📊</span> Statistiques en direct</h2>
<div class="live-stats" id="liveStats">
<div class="stat-row"><span>Workflow:</span><span id="liveWorkflow">-</span></div>
<div class="stat-row"><span>Mode:</span><span id="liveMode">-</span></div>
<div class="stat-row"><span>Node actuel:</span><span id="liveNode">-</span></div>
<div class="stat-row"><span>Étapes:</span><span id="liveSteps">0</span></div>
<div class="stat-row"><span>Confiance:</span><span id="liveConfidence">-</span></div>
</div>
</div>
</div>
<div class="card">
<h2><span class="icon">📜</span> Historique d'exécution</h2>
<div class="history-list" id="executionHistory"></div>
</div>
</div>
<!-- Tab: Workflows -->
<div id="tab-workflows" class="tab-content">
<div class="card">
<h2><span class="icon">🔄</span> Workflows disponibles</h2>
<div class="actions-bar" style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px;">
<div style="display:flex;gap:10px;align-items:center;">
<button class="btn btn-primary btn-small" onclick="refreshWorkflows()">🔄 Actualiser</button>
<label style="display:flex;align-items:center;gap:5px;font-size:13px;color:#94a3b8;cursor:pointer;">
<input type="checkbox" id="hideUnnamedWorkflows" checked onchange="refreshWorkflows()">
Masquer "Unnamed Workflow"
</label>
</div>
<div style="display:flex;gap:10px;align-items:center;">
<span id="unnamedWorkflowsInfo" style="font-size:12px;color:#f59e0b;"></span>
<button class="btn btn-warning btn-small" onclick="cleanupUnnamedWorkflows()" id="btnCleanupWorkflows" style="display:none;">
🗑️ Supprimer invalides
</button>
</div>
</div>
<div class="workflow-grid" id="workflowList">
<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 -->
<div id="tab-sessions" class="tab-content">
<div class="card">
<h2><span class="icon">📦</span> Sessions Agent</h2>
<div class="actions-bar" style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px;">
<div style="display:flex;gap:10px;align-items:center;">
<button class="btn btn-primary btn-small" onclick="refreshSessions()">🔄 Actualiser</button>
<label style="display:flex;align-items:center;gap:5px;font-size:13px;color:#94a3b8;cursor:pointer;">
<input type="checkbox" id="hideEmptySessions" checked onchange="refreshSessions()">
Masquer sessions vides
</label>
</div>
<div style="display:flex;gap:10px;align-items:center;">
<span id="emptySessionsInfo" style="font-size:12px;color:#f59e0b;"></span>
<button class="btn btn-warning btn-small" onclick="cleanupEmptySessions()" id="btnCleanup" style="display:none;">
🗑️ Supprimer vides
</button>
</div>
</div>
<div class="session-list" id="sessionList">
<div class="loading"><div class="spinner"></div>Chargement...</div>
</div>
</div>
<!-- Modal pour les screenshots -->
<div id="screenshotModal" class="modal" style="display:none;">
<div class="modal-content">
<span class="modal-close" onclick="closeModal()">&times;</span>
<h3 id="modalTitle">Screenshots de la session</h3>
<div class="screenshot-gallery" id="screenshotGallery"></div>
</div>
</div>
</div>
<!-- Tab: Performance -->
<div id="tab-performance" class="tab-content">
<!-- FAISS Status Panel -->
<div class="card" style="margin-bottom:20px;">
<div style="display:flex;justify-content:space-between;align-items:center;">
<h2><span class="icon">🗄️</span> État de l'Index FAISS</h2>
<button class="btn btn-primary btn-small" onclick="testFaissIndex()" id="btnTestFaiss">
🧪 Tester l'index
</button>
</div>
<div class="grid grid-4" style="margin-top:15px;">
<div style="text-align:center;cursor:help;" title="État de l'index FAISS&#10;✅ Actif = Index chargé et fonctionnel&#10;⚠️ Non trouvé = Aucun index créé&#10;❌ Erreur = Problème de chargement">
<div id="faissStatus" style="font-size:24px;"></div>
<div style="font-size:12px;color:#64748b;margin-top:5px;">Status <span style="color:#3b82f6;"></span></div>
</div>
<div style="text-align:center;cursor:help;" title="Nombre de vecteurs dans l'index&#10;Chaque cible cliquée génère un vecteur&#10;Plus il y a de vecteurs, plus la reconnaissance est précise">
<div id="faissVectors" style="font-size:24px;color:#3b82f6;font-weight:bold;">0</div>
<div style="font-size:12px;color:#64748b;margin-top:5px;">Vecteurs <span style="color:#3b82f6;"></span></div>
</div>
<div style="text-align:center;cursor:help;" title="Taille des vecteurs d'embedding&#10;512 = CLIP ViT-B/32 (standard)&#10;768 = CLIP ViT-L/14 (plus précis)">
<div id="faissDimensions" style="font-size:24px;color:#8b5cf6;font-weight:bold;">-</div>
<div style="font-size:12px;color:#64748b;margin-top:5px;">Dimensions <span style="color:#3b82f6;"></span></div>
</div>
<div style="text-align:center;cursor:help;" title="Type d'index FAISS&#10;Flat = Recherche exhaustive (précis, lent sur gros volumes)&#10;IVF = Index inversé (rapide, recommandé > 10k vecteurs)">
<div id="faissIndexType" style="font-size:24px;color:#f59e0b;font-weight:bold;">-</div>
<div style="font-size:12px;color:#64748b;margin-top:5px;">Type <span style="color:#3b82f6;"></span></div>
</div>
</div>
<div style="margin-top:15px;padding:10px;background:#0f172a;border-radius:8px;font-size:13px;color:#94a3b8;">
<span id="faissDetails">Chargement des détails FAISS...</span>
</div>
<!-- Zone de recommandations -->
<div id="faissRecommendations" style="display:none;margin-top:10px;padding:10px;background:#422006;border:1px solid #f59e0b;border-radius:8px;font-size:13px;color:#fbbf24;">
</div>
<!-- Résultats du test -->
<div id="faissTestResults" style="display:none;margin-top:15px;padding:15px;background:#0f172a;border-radius:8px;">
</div>
</div>
<div class="grid grid-3">
<div class="card stat-card">
<div class="stat-value" id="perfEmbeddings">0</div>
<div class="stat-label">Embeddings</div>
</div>
<div class="card stat-card">
<div class="stat-value" id="perfCacheHits">0%</div>
<div class="stat-label">Cache Hit Rate</div>
</div>
<div class="card stat-card">
<div class="stat-value" id="perfAvgTime">0ms</div>
<div class="stat-label">Temps moyen</div>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<h2><span class="icon">🔍</span> FAISS Performance</h2>
<canvas id="faissChart" height="250"></canvas>
</div>
<div class="card">
<h2><span class="icon">💾</span> Cache Performance</h2>
<canvas id="cacheChart" height="250"></canvas>
</div>
</div>
<!-- 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>
</div>
<div class="card stat-card">
<div class="stat-value" id="metricChains">0</div>
<div class="stat-label">Chaînes Exécutées</div>
</div>
<div class="card stat-card">
<div class="stat-value" id="metricTriggers">0</div>
<div class="stat-label">Triggers Activés</div>
</div>
<div class="card stat-card">
<div class="stat-value" id="metricErrors">0%</div>
<div class="stat-label">Taux d'Erreur</div>
</div>
</div>
<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>
<button class="btn btn-success btn-small" id="automationToggle" onclick="toggleAutomation()">▶️ Démarrer</button>
</div>
<div class="live-stats" id="automationStatus">
<div class="stat-row"><span>Statut:</span><span id="autoStatus">-</span></div>
<div class="stat-row"><span>Triggers actifs:</span><span id="autoTriggers">-</span></div>
<div class="stat-row"><span>Intervalle:</span><span id="autoInterval">-</span></div>
</div>
</div>
<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>
<button class="btn btn-secondary btn-small" onclick="window.open('/metrics', '_blank')">🔗 Endpoint /metrics</button>
</div>
<div class="test-output" id="metricsOutput">Chargement des métriques...</div>
</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">
<h2><span class="icon">📄</span> Logs Système</h2>
<div class="actions-bar">
<button class="btn btn-primary btn-small" onclick="refreshLogs()">🔄 Actualiser</button>
<button class="btn btn-success btn-small" onclick="downloadLogs()">📥 Télécharger ZIP</button>
</div>
<div class="test-output" id="logsOutput" style="max-height: 600px;">Chargement des logs...</div>
</div>
</div>
<!-- Tab: Tests -->
<div id="tab-tests" class="tab-content">
<div class="grid grid-2">
<div class="card">
<h2><span class="icon">🧪</span> Tests disponibles</h2>
<div class="actions-bar">
<button class="btn btn-success btn-small" onclick="runAllTests('unit')">▶️ Unit</button>
<button class="btn btn-primary btn-small" onclick="runAllTests('integration')">▶️ Integration</button>
<button class="btn btn-warning btn-small" onclick="runAllTests('performance')">▶️ Performance</button>
</div>
<div class="test-list" id="testList">
<div class="loading"><div class="spinner"></div>Chargement...</div>
</div>
</div>
<div class="card">
<h2><span class="icon">📋</span> Sortie des tests</h2>
<div class="test-output" id="testOutput">Sélectionnez un test pour voir la sortie</div>
</div>
</div>
</div>
<!-- Tab: Sauvegardes -->
<div id="tab-backups" 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> Sauvegardes & Export</h2>
<p style="color:#64748b;font-size:13px;">Exportez et téléchargez vos données pour la sauvegarde ou le transfert</p>
</div>
<button class="btn btn-primary" onclick="refreshBackupStats()">🔄 Actualiser</button>
</div>
</div>
<div class="grid grid-4" id="backupStats">
<div class="card stat-card">
<div class="stat-value" id="statWorkflowsBackup">-</div>
<div class="stat-label">Workflows</div>
</div>
<div class="card stat-card">
<div class="stat-value" id="statCorrectionsBackup">-</div>
<div class="stat-label">Corrections</div>
</div>
<div class="card stat-card">
<div class="stat-value" id="statModelsBackup">-</div>
<div class="stat-label">Modèles</div>
</div>
<div class="card stat-card">
<div class="stat-value" id="statSessionsBackup">-</div>
<div class="stat-label">Sessions</div>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<h2><span class="icon">📦</span> Export par catégorie</h2>
<div class="backup-list">
<div class="backup-item" onclick="downloadBackup('workflows')">
<div class="backup-icon">🔄</div>
<div class="backup-info">
<h4>Workflows</h4>
<p>Tous les workflows et templates</p>
</div>
<button class="btn btn-primary btn-small">⬇️ Télécharger</button>
</div>
<div class="backup-item" onclick="downloadBackup('correction-packs')">
<div class="backup-icon">🔧</div>
<div class="backup-info">
<h4>Correction Packs</h4>
<p>Packs de corrections cross-workflow</p>
</div>
<button class="btn btn-primary btn-small">⬇️ Télécharger</button>
</div>
<div class="backup-item" onclick="downloadBackup('trained-models')">
<div class="backup-icon">🧠</div>
<div class="backup-info">
<h4>Modèles entraînés</h4>
<p>Index FAISS et embeddings</p>
</div>
<button class="btn btn-primary btn-small">⬇️ Télécharger</button>
</div>
<div class="backup-item" onclick="downloadBackup('config')">
<div class="backup-icon">⚙️</div>
<div class="backup-info">
<h4>Configuration</h4>
<p>Paramètres système (secrets masqués)</p>
</div>
<button class="btn btn-primary btn-small">⬇️ Télécharger</button>
</div>
</div>
</div>
<div class="card">
<h2><span class="icon">📁</span> Backup complet</h2>
<div style="padding:20px;text-align:center;">
<div style="font-size:48px;margin-bottom:20px;">📥</div>
<p style="color:#94a3b8;margin-bottom:20px;">Téléchargez une archive complète de toutes vos données</p>
<button class="btn btn-success" style="font-size:16px;padding:15px 30px;" onclick="downloadFullBackup()">
⬇️ Télécharger le backup complet
</button>
<p style="color:#64748b;font-size:12px;margin-top:15px;">Inclut: workflows, corrections, modèles, configuration</p>
</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%);">
<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> Correction Packs</h2>
<p style="color:#64748b;font-size:13px;">Capitalisez les corrections utilisateur pour améliorer l'auto-healing cross-workflow</p>
</div>
<div style="display:flex;gap:10px;">
<button class="btn btn-success" onclick="showCreatePackModal()"> Nouveau Pack</button>
<button class="btn btn-primary" onclick="refreshCorrectionPacks()">🔄 Actualiser</button>
</div>
</div>
</div>
<div class="grid grid-4">
<div class="card stat-card">
<div class="stat-value" id="statPacks">-</div>
<div class="stat-label">Packs</div>
</div>
<div class="card stat-card">
<div class="stat-value" id="statTotalCorrections">-</div>
<div class="stat-label">Corrections</div>
</div>
<div class="card stat-card">
<div class="stat-value" id="statApplications">-</div>
<div class="stat-label">Applications</div>
</div>
<div class="card stat-card">
<div class="stat-value" id="statSuccessRate">-</div>
<div class="stat-label">Taux succès</div>
</div>
</div>
<div class="card">
<h2><span class="icon">📦</span> Packs disponibles</h2>
<div class="correction-packs-list" id="correctionPacksList">
<div class="loading"><div class="spinner"></div>Chargement...</div>
</div>
</div>
<!-- Modal création pack -->
<div id="createPackModal" class="modal" style="display:none;">
<div class="modal-content" style="max-width:500px;">
<span class="modal-close" onclick="closeCreatePackModal()">&times;</span>
<h3> Créer un nouveau pack</h3>
<div style="margin-top:20px;">
<label style="display:block;color:#94a3b8;margin-bottom:5px;">Nom du pack</label>
<input type="text" id="packName" class="select-input" placeholder="Ex: Corrections SAP">
<label style="display:block;color:#94a3b8;margin:15px 0 5px;">Description</label>
<textarea id="packDescription" class="select-input" rows="3" placeholder="Description optionnelle..."></textarea>
<label style="display:block;color:#94a3b8;margin:15px 0 5px;">Catégorie</label>
<input type="text" id="packCategory" class="select-input" placeholder="Ex: erp, web, desktop">
<div style="display:flex;gap:10px;margin-top:20px;">
<button class="btn btn-success" onclick="createCorrectionPack()">✅ Créer</button>
<button class="btn btn-secondary" onclick="closeCreatePackModal()">Annuler</button>
</div>
</div>
</div>
</div>
</div>
<!-- Tab: Apprentissage -->
<div id="tab-learning" 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> Apprentissage & Corpus</h2>
<p style="color:#64748b;font-size:13px;">Statistiques d'apprentissage et évolution du corpus de données</p>
</div>
<button class="btn btn-primary" onclick="refreshLearningStats()">🔄 Actualiser</button>
</div>
</div>
<div class="grid grid-4">
<div class="card stat-card">
<div class="stat-value" id="statCorpusSize">-</div>
<div class="stat-label">Taille corpus</div>
</div>
<div class="card stat-card">
<div class="stat-value" id="statTrainedSessions">-</div>
<div class="stat-label">Sessions traitées</div>
</div>
<div class="card stat-card">
<div class="stat-value" id="statHealingSuccess">-</div>
<div class="stat-label">Self-healing</div>
</div>
<div class="card stat-card">
<div class="stat-value" id="statLearningRate">-</div>
<div class="stat-label">Taux apprentissage</div>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<h2><span class="icon">📈</span> Évolution du corpus</h2>
<canvas id="corpusChart" height="250"></canvas>
</div>
<div class="card">
<h2><span class="icon">🎯</span> Performance self-healing</h2>
<canvas id="healingChart" height="250"></canvas>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<h2><span class="icon">📊</span> Répartition par type d'action</h2>
<div id="actionTypeStats" style="padding:15px;">
<div class="loading"><div class="spinner"></div>Chargement...</div>
</div>
</div>
<div class="card">
<h2><span class="icon">🏆</span> Top corrections appliquées</h2>
<div id="topCorrections" style="padding:15px;">
<div class="loading"><div class="spinner"></div>Chargement...</div>
</div>
</div>
</div>
</div>
<!-- Tab: Configuration -->
<div id="tab-config" 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> Configuration Systeme</h2>
<p style="color:#64748b;font-size:13px;">Configurez les services, modeles LLM/VLM, base de donnees et parametres de securite</p>
</div>
<div style="display:flex;gap:10px;">
<button class="btn btn-success" onclick="saveConfig()">💾 Sauvegarder</button>
<button class="btn btn-primary" onclick="refreshConfig()">🔄 Actualiser</button>
<button class="btn btn-secondary" onclick="exportConfig()">📥 Exporter</button>
<label class="btn btn-secondary" style="cursor:pointer;">
📤 Importer
<input type="file" id="importConfigFile" accept=".json" style="display:none;" onchange="importConfig(this)">
</label>
</div>
</div>
</div>
<div class="grid grid-2">
<!-- Section Services -->
<div class="card">
<h2><span class="icon">🌐</span> Services & Ports</h2>
<div id="configServices" class="config-section">
<div class="config-item">
<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="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 (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="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 (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="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>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;">
<button class="btn btn-small btn-primary" onclick="testConnection('service', 'upload_api')">Test</button>
</div>
</div>
<div class="config-item">
<label>Session Cleaner (port 5006)</label>
<div style="display:flex;gap:10px;">
<input type="text" id="cfg_session_cleaner_host" placeholder="localhost" class="config-input" style="flex:2;">
<input type="number" id="cfg_session_cleaner_port" placeholder="5006" class="config-input" style="flex:1;">
<button class="btn btn-small btn-primary" onclick="testConnection('service', 'session_cleaner')">Test</button>
</div>
</div>
</div>
</div>
<!-- Section LLM/VLM -->
<div class="card">
<h2><span class="icon">🤖</span> Modeles LLM & VLM</h2>
<div id="configLLM" class="config-section">
<div class="config-item">
<label>Provider</label>
<select id="cfg_llm_provider" class="config-input">
<option value="ollama">Ollama (local)</option>
<option value="openai">OpenAI</option>
<option value="anthropic">Anthropic</option>
</select>
</div>
<div class="config-item">
<label>URL Ollama</label>
<div style="display:flex;gap:10px;">
<input type="text" id="cfg_llm_base_url" placeholder="http://localhost:11434" class="config-input" style="flex:1;">
<button class="btn btn-small btn-primary" onclick="testOllamaConnection()">Test</button>
</div>
</div>
<div class="config-item">
<label>Modele LLM</label>
<div style="display:flex;gap:10px;">
<select id="cfg_llm_model" class="config-input" style="flex:1;">
<option value="">Chargement...</option>
</select>
<button class="btn btn-small btn-secondary" onclick="refreshOllamaModels()">🔄</button>
</div>
</div>
<div class="config-item">
<label>Modele VLM</label>
<select id="cfg_vlm_model" class="config-input">
<option value="">Chargement...</option>
</select>
</div>
<div class="config-item">
<label>Temperature</label>
<input type="range" id="cfg_llm_temperature" min="0" max="2" step="0.1" value="0.7" class="config-input" oninput="document.getElementById('tempValue').textContent = this.value">
<span id="tempValue" style="color:#3b82f6;margin-left:10px;">0.7</span>
</div>
<div class="config-item">
<label>Max Tokens</label>
<input type="number" id="cfg_llm_max_tokens" placeholder="2048" class="config-input" value="2048">
</div>
</div>
</div>
</div>
<div class="grid grid-2">
<!-- Section Base de donnees -->
<div class="card">
<h2><span class="icon">💾</span> Base de Donnees</h2>
<div id="configDatabase" class="config-section">
<div class="config-item">
<label>Type</label>
<select id="cfg_database_type" class="config-input">
<option value="sqlite">SQLite (local)</option>
<option value="postgresql">PostgreSQL</option>
<option value="mysql">MySQL</option>
</select>
</div>
<div class="config-item">
<label>Chemin / URL</label>
<div style="display:flex;gap:10px;">
<input type="text" id="cfg_database_path" placeholder="data/training/workflows.db" class="config-input" style="flex:1;">
<button class="btn btn-small btn-primary" onclick="testDatabaseConnection()">Test</button>
</div>
</div>
<div class="config-item">
<label>
<input type="checkbox" id="cfg_database_backup_enabled" checked>
Backup automatique
</label>
</div>
<div class="config-item">
<label>Intervalle backup (heures)</label>
<input type="number" id="cfg_database_backup_interval" value="24" class="config-input">
</div>
</div>
</div>
<!-- Section Securite -->
<div class="card">
<h2><span class="icon">🔒</span> Securite</h2>
<div id="configSecurity" class="config-section">
<div class="config-item">
<label>
<input type="checkbox" id="cfg_security_encryption" checked>
Chiffrement des donnees
</label>
</div>
<div class="config-item">
<label>
<input type="checkbox" id="cfg_security_auth">
Authentification requise
</label>
</div>
<div class="config-item">
<label>Timeout session (minutes)</label>
<input type="number" id="cfg_security_timeout" value="60" class="config-input">
</div>
<div class="config-item">
<label>Origines autorisees (CORS)</label>
<input type="text" id="cfg_security_origins" placeholder="http://localhost:3002,http://localhost:5001,http://localhost:5002" class="config-input">
</div>
</div>
</div>
<!-- Section Logs -->
<div class="card">
<h2><span class="icon">📝</span> Logs</h2>
<div id="configLogging" class="config-section">
<div class="config-item">
<label>Niveau de log</label>
<select id="cfg_logging_level" class="config-input">
<option value="DEBUG">DEBUG</option>
<option value="INFO" selected>INFO</option>
<option value="WARNING">WARNING</option>
<option value="ERROR">ERROR</option>
</select>
</div>
<div class="config-item">
<label>Fichier de log</label>
<input type="text" id="cfg_logging_file" placeholder="logs/rpa_vision.log" class="config-input">
</div>
<div class="config-item">
<label>Taille max (MB)</label>
<input type="number" id="cfg_logging_max_size" value="50" class="config-input">
</div>
<div class="config-item">
<label>Nombre de backups</label>
<input type="number" id="cfg_logging_backup_count" value="5" class="config-input">
</div>
</div>
</div>
</div>
<!-- Resultat des tests -->
<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>
</div>
</div>
</div>
<!-- Tab: Nettoyage de sessions (iframe vers session_cleaner port 5006) -->
<div id="tab-cleaner" 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> Nettoyage de sessions avant replay</h2>
<p style="color:#64748b;font-size:13px;">Visualisez les sessions, supprimez les clics parasites et regénérez un replay propre (Session Cleaner, port 5006)</p>
</div>
<div style="display:flex;gap:10px;">
<button class="btn btn-primary" onclick="refreshCleanerFrame()">🔄 Recharger</button>
<a class="btn btn-secondary" href="http://localhost:5006" target="_blank" rel="noopener">↗ Ouvrir dans un onglet</a>
</div>
</div>
</div>
<!-- Message d'état (affiché si le service n'est pas démarré) -->
<div id="cleanerOfflineNotice" class="card" style="display:none;margin-bottom:20px;border:1px solid #ef4444;">
<div style="display:flex;align-items:center;gap:20px;flex-wrap:wrap;">
<div style="font-size:48px;">⚠️</div>
<div style="flex:1;min-width:250px;">
<h3 style="color:#ef4444;margin-bottom:8px;">Session Cleaner non démarré</h3>
<p style="color:#94a3b8;font-size:13px;">Le service sur le port 5006 ne répond pas. Démarrez-le pour accéder à l'interface de nettoyage.</p>
</div>
<div style="display:flex;gap:10px;">
<button class="btn btn-success" onclick="startCleanerService()" id="btnStartCleaner">▶️ Démarrer le cleaner</button>
<button class="btn btn-secondary" onclick="switchTab('services')">🎛️ Gérer les services</button>
</div>
</div>
</div>
<!-- iframe vers le cleaner -->
<div id="cleanerFrameContainer" class="card" style="padding:0;overflow:hidden;">
<iframe
id="cleanerFrame"
src="about:blank"
style="width:100%;height:85vh;min-height:800px;border:0;border-radius:12px;background:#0f172a;"
title="Session Cleaner"></iframe>
</div>
</div>
</div>
<style>
.execution-panel { padding: 15px; background: #0f172a; border-radius: 8px; }
/* Configuration styles */
.config-section { display: flex; flex-direction: column; gap: 15px; padding: 10px 0; }
.config-item { display: flex; flex-direction: column; gap: 5px; }
.config-item label { color: #94a3b8; font-size: 13px; font-weight: 500; display: flex; align-items: center; gap: 8px; }
.config-item label input[type="checkbox"] { width: 18px; height: 18px; accent-color: #3b82f6; }
.config-input { width: 100%; padding: 10px 12px; background: #0f172a; border: 1px solid #334155; border-radius: 8px; color: #e2e8f0; font-size: 14px; transition: border-color 0.2s; }
.config-input:focus { outline: none; border-color: #3b82f6; }
.config-input:hover { border-color: #475569; }
select.config-input { cursor: pointer; }
input[type="range"].config-input { padding: 0; height: 6px; border: none; cursor: pointer; }
.exec-idle { color: #64748b; text-align: center; padding: 30px; }
.exec-running { color: #22c55e; }
.exec-controls { display: flex; flex-direction: column; gap: 15px; }
.select-input { width: 100%; padding: 12px; background: #0f172a; border: 1px solid #334155; border-radius: 8px; color: #e2e8f0; font-size: 14px; }
.btn-group { display: flex; gap: 10px; }
.btn { padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s; }
.btn-primary { background: #3b82f6; color: white; }
.btn-primary:hover { background: #2563eb; }
.btn-success { background: #22c55e; color: white; }
.btn-success:hover { background: #16a34a; }
.btn-warning { background: #f59e0b; color: white; }
.btn-warning:hover { background: #d97706; }
.btn-danger { background: #ef4444; color: white; }
.btn-danger:hover { background: #dc2626; }
.btn-small { padding: 8px 16px; font-size: 12px; }
.live-stats { display: flex; flex-direction: column; gap: 12px; }
.stat-row { display: flex; justify-content: space-between; padding: 10px; background: #0f172a; border-radius: 6px; }
.stat-row span:first-child { color: #64748b; }
.stat-row span:last-child { font-weight: 600; color: #3b82f6; }
.history-list { max-height: 400px; overflow-y: auto; }
.history-item { display: flex; align-items: center; gap: 15px; padding: 12px; border-bottom: 1px solid #334155; }
.history-item .time { color: #64748b; font-size: 12px; min-width: 80px; }
.history-item .node { flex: 1; }
.history-item .confidence { color: #3b82f6; font-weight: 600; }
.history-item.success { border-left: 3px solid #22c55e; }
.history-item.failed { border-left: 3px solid #ef4444; }
.workflow-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 15px; }
.workflow-card { background: #0f172a; border-radius: 8px; padding: 20px; border: 1px solid #334155; transition: all 0.2s; }
.workflow-card:hover { border-color: #3b82f6; transform: translateY(-2px); }
.workflow-card h3 { color: #e2e8f0; margin-bottom: 10px; }
.workflow-card .meta { color: #64748b; font-size: 12px; margin-bottom: 15px; }
.workflow-card .badge { display: inline-block; padding: 4px 10px; border-radius: 20px; font-size: 11px; font-weight: 600; }
.badge-observation { background: #1e40af; color: #93c5fd; }
.badge-coaching { background: #7c3aed; color: #c4b5fd; }
.badge-supervised { background: #059669; color: #6ee7b7; }
.badge-automatic { background: #dc2626; color: #fca5a5; }
.session-list { max-height: 600px; overflow-y: auto; }
.session-item { display: flex; justify-content: space-between; align-items: center; padding: 15px; border-bottom: 1px solid #334155; transition: background 0.2s; }
.session-item:hover { background: #334155; }
.session-info h4 { color: #e2e8f0; margin-bottom: 5px; }
.session-info .meta { color: #64748b; font-size: 12px; }
.session-actions { display: flex; gap: 8px; }
.actions-bar { display: flex; gap: 10px; margin-bottom: 15px; }
.test-list { max-height: 500px; overflow-y: auto; }
.test-item { display: flex; justify-content: space-between; align-items: center; padding: 12px; border-bottom: 1px solid #334155; cursor: pointer; }
.test-item:hover { background: #334155; }
.test-type { padding: 4px 10px; border-radius: 4px; font-size: 11px; font-weight: 600; }
.test-type.unit { background: #1e40af; color: #93c5fd; }
.test-type.integration { background: #7c3aed; color: #c4b5fd; }
.test-type.performance { background: #f59e0b; color: #fef3c7; }
.test-output { background: #0f172a; padding: 15px; border-radius: 8px; font-family: monospace; font-size: 12px; max-height: 500px; overflow-y: auto; white-space: pre-wrap; color: #94a3b8; }
.loading { text-align: center; padding: 40px; color: #64748b; }
.spinner { border: 3px solid #334155; border-top: 3px solid #3b82f6; border-radius: 50%; width: 30px; height: 30px; animation: spin 1s linear infinite; margin: 0 auto 10px; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 1000; display: flex; align-items: center; justify-content: center; }
.modal-content { background: #1e293b; border-radius: 12px; padding: 30px; max-width: 90%; max-height: 90%; overflow: auto; }
.modal-close { position: absolute; top: 20px; right: 30px; font-size: 30px; cursor: pointer; color: #94a3b8; }
.screenshot-gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 15px; margin-top: 20px; }
.screenshot-gallery img { width: 100%; border-radius: 8px; cursor: pointer; transition: transform 0.2s; }
.screenshot-gallery img:hover { transform: scale(1.05); }
/* Services Panel Styles */
.service-card { background: #1e293b; border-radius: 12px; padding: 20px; border: 2px solid #334155; transition: all 0.3s; position: relative; overflow: hidden; }
.service-card.running { border-color: #22c55e; box-shadow: 0 0 20px rgba(34, 197, 94, 0.2); }
.service-card.stopped { border-color: #ef4444; }
.service-card .status-indicator { position: absolute; top: 15px; right: 15px; width: 16px; height: 16px; border-radius: 50%; animation: pulse 2s infinite; }
.service-card .status-indicator.running { background: #22c55e; }
.service-card .status-indicator.stopped { background: #ef4444; animation: none; }
.service-card .service-icon { font-size: 36px; margin-bottom: 10px; }
.service-card h3 { color: #e2e8f0; font-size: 18px; margin-bottom: 5px; }
.service-card .description { color: #64748b; font-size: 13px; margin-bottom: 15px; }
.service-card .port-info { display: inline-block; padding: 4px 10px; background: #0f172a; border-radius: 4px; font-size: 12px; color: #94a3b8; margin-bottom: 15px; }
.service-card .actions { display: flex; gap: 8px; flex-wrap: wrap; }
.service-card .btn { flex: 1; min-width: 80px; }
.quick-link { display: flex; align-items: center; gap: 12px; padding: 15px 20px; background: #0f172a; border-radius: 10px; border: 1px solid #334155; cursor: pointer; transition: all 0.2s; text-decoration: none; color: #e2e8f0; }
.quick-link:hover { border-color: #3b82f6; transform: translateY(-2px); background: #1e293b; }
.quick-link .icon { font-size: 24px; }
.quick-link .info { flex: 1; }
.quick-link .name { font-weight: 600; margin-bottom: 2px; }
.quick-link .url { font-size: 12px; color: #64748b; }
.quick-link .status-badge { padding: 4px 10px; border-radius: 20px; font-size: 11px; font-weight: 600; }
.quick-link .status-badge.running { background: #052e16; color: #22c55e; }
.quick-link .status-badge.stopped { background: #450a0a; color: #ef4444; }
.btn-secondary { background: #475569; color: white; }
.btn-secondary:hover { background: #64748b; }
/* Backup section styles */
.backup-list { display: flex; flex-direction: column; gap: 10px; }
.backup-item { display: flex; align-items: center; gap: 15px; padding: 15px; background: #0f172a; border-radius: 10px; border: 1px solid #334155; cursor: pointer; transition: all 0.2s; }
.backup-item:hover { border-color: #3b82f6; transform: translateX(5px); }
.backup-icon { font-size: 28px; }
.backup-info { flex: 1; }
.backup-info h4 { color: #e2e8f0; margin-bottom: 3px; }
.backup-info p { color: #64748b; font-size: 12px; }
/* System info styles */
.system-info { display: flex; flex-direction: column; gap: 10px; }
.system-info-row { display: flex; justify-content: space-between; padding: 10px 15px; background: #0f172a; border-radius: 6px; }
.system-info-row span:first-child { color: #64748b; }
.system-info-row span:last-child { color: #e2e8f0; font-weight: 500; }
/* Correction packs styles */
.correction-packs-list { display: flex; flex-direction: column; gap: 12px; }
.correction-pack-item { display: flex; justify-content: space-between; align-items: center; padding: 20px; background: #0f172a; border-radius: 10px; border: 1px solid #334155; transition: all 0.2s; }
.correction-pack-item:hover { border-color: #3b82f6; }
.pack-main { display: flex; align-items: center; gap: 15px; flex: 1; }
.pack-icon { font-size: 32px; }
.pack-details h4 { color: #e2e8f0; margin-bottom: 5px; }
.pack-details p { color: #64748b; font-size: 12px; }
.pack-stats { display: flex; gap: 20px; }
.pack-stat { text-align: center; }
.pack-stat .value { font-size: 18px; font-weight: bold; color: #3b82f6; }
.pack-stat .label { font-size: 10px; color: #64748b; text-transform: uppercase; }
.pack-actions { display: flex; gap: 8px; margin-left: 20px; }
/* Learning stats styles */
.progress-bar { height: 8px; background: #334155; border-radius: 4px; overflow: hidden; margin-top: 8px; }
.progress-fill { height: 100%; background: linear-gradient(90deg, #3b82f6, #8b5cf6); border-radius: 4px; transition: width 0.5s ease; }
.stat-bar { display: flex; justify-content: space-between; align-items: center; padding: 12px 15px; background: #0f172a; border-radius: 8px; margin-bottom: 8px; }
.stat-bar-label { color: #94a3b8; }
.stat-bar-value { color: #3b82f6; font-weight: 600; }
/* Version backup item */
.version-backup-item { display: flex; justify-content: space-between; align-items: center; padding: 12px 15px; background: #0f172a; border-radius: 8px; margin-bottom: 8px; border: 1px solid #334155; }
.version-backup-item:hover { border-color: #3b82f6; }
.version-backup-info { flex: 1; }
.version-backup-info h5 { color: #e2e8f0; margin-bottom: 3px; font-weight: 500; }
.version-backup-info span { color: #64748b; font-size: 12px; }
</style>
<script>
// WebSocket connection
const socket = io();
let perfChart, faissChart, cacheChart;
let executionState = { running: false };
// === HELPER: Validated JSON fetch ===
async function fetchJSON(url, options = {}) {
const response = await fetch(url, options);
if (!response.ok) {
const text = await response.text();
// Check if it's HTML (error page)
if (text.trim().startsWith('<')) {
throw new Error(`Serveur erreur ${response.status}: ${response.statusText}`);
}
throw new Error(text || `HTTP ${response.status}`);
}
return response.json();
}
// === HELPER: Show error in UI ===
function showError(elementId, message) {
const el = document.getElementById(elementId);
if (el) {
el.innerHTML = `<p style="color:#ef4444;padding:20px;text-align:center;">❌ ${message}</p>`;
}
}
// Socket events
socket.on('connect', () => {
document.getElementById('statusDot').classList.remove('offline');
document.getElementById('statusText').textContent = 'Connecté';
});
socket.on('disconnect', () => {
document.getElementById('statusDot').classList.add('offline');
document.getElementById('statusText').textContent = 'Déconnecté';
});
socket.on('execution_started', (data) => {
executionState = data;
updateExecutionUI();
});
socket.on('execution_step', (data) => {
executionState = data;
updateExecutionUI();
addHistoryItem(data.history[data.history.length - 1]);
});
socket.on('execution_stopped', (data) => {
executionState = data;
updateExecutionUI();
});
socket.on('metrics_update', (data) => {
if (data.execution) executionState = data.execution;
updateExecutionUI();
});
// Tab switching
function switchTab(tabName) {
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');
// 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(); refreshSystemInfo(); }
if (tabName === 'sessions') refreshSessions();
if (tabName === 'workflows') { refreshWorkflows(); refreshChains(); refreshTriggers(); }
if (tabName === 'tests') refreshTests();
if (tabName === 'performance') { refreshPerformance(); refreshMetrics(); refreshAutomationStatus(); }
if (tabName === 'streaming') refreshStreaming();
if (tabName === 'logs') refreshLogs();
if (tabName === 'backups') refreshBackupStats();
if (tabName === 'corrections') refreshCorrectionPacks();
if (tabName === 'learning') refreshLearningStats();
if (tabName === 'config') refreshConfig();
if (tabName === 'cleaner') checkCleanerStatus();
}
// === Session Cleaner (iframe vers port 5006) ===
const CLEANER_URL = 'http://localhost:5006';
async function checkCleanerStatus() {
const notice = document.getElementById('cleanerOfflineNotice');
const frameContainer = document.getElementById('cleanerFrameContainer');
const frame = document.getElementById('cleanerFrame');
if (!notice || !frameContainer || !frame) return;
try {
const res = await fetch('/api/services/session_cleaner/status');
const data = await res.json();
const running = data && data.status === 'running';
if (running) {
notice.style.display = 'none';
frameContainer.style.display = 'block';
// Charger l'iframe seulement si ce n'est pas déjà fait
if (frame.src === 'about:blank' || !frame.src.startsWith(CLEANER_URL)) {
frame.src = CLEANER_URL;
}
} else {
notice.style.display = 'block';
frameContainer.style.display = 'none';
frame.src = 'about:blank';
}
} catch (err) {
console.error('checkCleanerStatus error:', err);
notice.style.display = 'block';
frameContainer.style.display = 'none';
}
}
function refreshCleanerFrame() {
const frame = document.getElementById('cleanerFrame');
if (!frame) return;
// Forcer un rechargement (cache busting)
frame.src = CLEANER_URL + '?t=' + Date.now();
}
async function startCleanerService() {
const btn = document.getElementById('btnStartCleaner');
if (btn) {
btn.disabled = true;
btn.textContent = '⏳ Démarrage...';
}
try {
const res = await fetch('/api/services/session_cleaner/start', { method: 'POST' });
const data = await res.json();
if (!res.ok) {
alert('Erreur : ' + (data.error || 'démarrage impossible'));
} else {
// Laisser le temps au service de démarrer
await new Promise(r => setTimeout(r, 1500));
}
} catch (err) {
alert('Erreur réseau : ' + err.message);
} finally {
if (btn) {
btn.disabled = false;
btn.textContent = '▶️ Démarrer le cleaner';
}
await checkCleanerStatus();
}
}
// Update execution UI
function updateExecutionUI() {
const panel = document.getElementById('executionStatus');
const statExec = document.getElementById('statExecution');
if (executionState.running) {
panel.innerHTML = `
<div class="exec-running">
<div style="font-size: 18px; margin-bottom: 10px;">🟢 Exécution en cours</div>
<div>Workflow: <strong>${executionState.workflow_id || '-'}</strong></div>
<div>Mode: <strong>${executionState.mode || '-'}</strong></div>
<div>Node: <strong>${executionState.current_node || '-'}</strong></div>
<div>Étapes: <strong>${executionState.steps_executed || 0}</strong></div>
<div>Confiance: <strong>${(executionState.last_confidence * 100).toFixed(1)}%</strong></div>
</div>
`;
statExec.textContent = '🟢';
statExec.style.color = '#22c55e';
} else {
panel.innerHTML = '<div class="exec-idle">Aucune exécution en cours</div>';
statExec.textContent = '⚪';
statExec.style.color = '#64748b';
}
// Update live stats
document.getElementById('liveWorkflow').textContent = executionState.workflow_id || '-';
document.getElementById('liveMode').textContent = executionState.mode || '-';
document.getElementById('liveNode').textContent = executionState.current_node || '-';
document.getElementById('liveSteps').textContent = executionState.steps_executed || 0;
document.getElementById('liveConfidence').textContent = executionState.last_confidence ?
(executionState.last_confidence * 100).toFixed(1) + '%' : '-';
}
function addHistoryItem(item) {
if (!item) return;
const list = document.getElementById('executionHistory');
const div = document.createElement('div');
div.className = `history-item ${item.success ? 'success' : 'failed'}`;
div.innerHTML = `
<span class="time">${new Date(item.timestamp).toLocaleTimeString()}</span>
<span class="node">${item.node_id}</span>
<span class="confidence">${(item.confidence * 100).toFixed(1)}%</span>
`;
list.insertBefore(div, list.firstChild);
}
// Execution controls
async function startExecution() {
const workflowId = document.getElementById('workflowSelect').value;
const mode = document.getElementById('modeSelect').value;
if (!workflowId) {
alert('Sélectionnez un workflow');
return;
}
try {
const data = await fetchJSON(`/api/workflows/${workflowId}/execute`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ mode })
});
if (data.error) alert('Erreur: ' + data.error);
} catch (e) {
alert('Erreur: ' + e.message);
}
}
async function stopExecution() {
if (!executionState.workflow_id) return;
try {
await fetchJSON(`/api/workflows/${executionState.workflow_id}/stop`, { method: 'POST' });
} catch (e) {
alert('Erreur arrêt: ' + e.message);
}
}
function pauseExecution() {
alert('Pause non implémentée - utilisez Stop');
}
// Refresh functions
async function refreshSystemStatus() {
try {
const data = await fetchJSON('/api/system/status');
document.getElementById('statSessions').textContent = data.sessions_count || 0;
document.getElementById('statWorkflows').textContent = data.workflows_count || 0;
document.getElementById('statTests').textContent = data.tests?.total || 0;
if (data.execution) {
executionState = data.execution;
updateExecutionUI();
}
} catch (e) {
console.error('Status error:', e);
document.getElementById('statSessions').textContent = '!';
document.getElementById('statWorkflows').textContent = '!';
document.getElementById('statTests').textContent = '!';
}
}
async function refreshWorkflows() {
const list = document.getElementById('workflowList');
list.innerHTML = '<div class="loading"><div class="spinner"></div>Chargement...</div>';
try {
const hideUnnamed = document.getElementById('hideUnnamedWorkflows').checked;
const data = await fetchJSON(`/api/workflows?hide_unnamed=${hideUnnamed}`);
// Afficher info sur workflows invalides
const infoEl = document.getElementById('unnamedWorkflowsInfo');
const btnCleanup = document.getElementById('btnCleanupWorkflows');
if (data.hidden_unnamed > 0) {
infoEl.textContent = `⚠️ ${data.hidden_unnamed} workflow(s) invalide(s)`;
btnCleanup.style.display = 'inline-block';
} else {
infoEl.textContent = '';
btnCleanup.style.display = 'none';
}
if (!data.workflows || data.workflows.length === 0) {
list.innerHTML = '<p style="text-align:center;color:#64748b;padding:40px;">Aucun workflow disponible</p>';
return;
}
let html = '';
const select = document.getElementById('workflowSelect');
select.innerHTML = '<option value="">Sélectionner un workflow...</option>';
data.workflows.forEach(wf => {
const badgeClass = `badge-${wf.learning_state.toLowerCase()}`;
html += `
<div class="workflow-card">
<h3>${wf.name || wf.workflow_id}</h3>
<div class="meta">${wf.nodes_count} nodes • ${wf.edges_count} edges • ${wf.execution_count} exécutions</div>
<span class="badge ${badgeClass}">${wf.learning_state}</span>
<p style="margin-top:10px;color:#94a3b8;font-size:13px;">${wf.description || 'Pas de description'}</p>
<div style="margin-top:15px;">
<button class="btn btn-primary btn-small" onclick="viewWorkflow('${wf.workflow_id}')">👁️ Voir</button>
<button class="btn btn-success btn-small" onclick="selectWorkflow('${wf.workflow_id}')">▶️ Exécuter</button>
</div>
</div>
`;
select.innerHTML += `<option value="${wf.workflow_id}">${wf.name || wf.workflow_id}</option>`;
});
list.innerHTML = html;
} catch (e) {
list.innerHTML = '<p style="color:#ef4444;">Erreur: ' + e.message + '</p>';
}
}
function selectWorkflow(id) {
document.getElementById('workflowSelect').value = id;
switchTab('execution');
}
async function viewWorkflow(id) {
try {
const data = await fetchJSON(`/api/workflows/${id}`);
alert(JSON.stringify(data.workflow, null, 2));
} catch (e) {
alert('Erreur: ' + e.message);
}
}
async function cleanupUnnamedWorkflows() {
if (!confirm('Êtes-vous sûr de vouloir supprimer tous les workflows "Unnamed Workflow" ?\n\nCette action est irréversible.')) {
return;
}
const btn = document.getElementById('btnCleanupWorkflows');
const originalText = btn.textContent;
btn.disabled = true;
btn.textContent = '⏳ Suppression...';
try {
const data = await fetchJSON('/api/workflows/cleanup-unnamed', { method: 'POST' });
alert(`${data.deleted_count} workflow(s) supprimé(s)`);
refreshWorkflows();
} catch (e) {
alert('❌ Erreur: ' + e.message);
} finally {
btn.disabled = false;
btn.textContent = originalText;
}
}
// Sessions
async function refreshSessions() {
const list = document.getElementById('sessionList');
list.innerHTML = '<div class="loading"><div class="spinner"></div>Chargement...</div>';
try {
const hideEmpty = document.getElementById('hideEmptySessions').checked;
const data = await fetchJSON(`/api/agent/sessions?hide_empty=${hideEmpty}`);
// Afficher info sur sessions vides
const infoEl = document.getElementById('emptySessionsInfo');
const btnCleanup = document.getElementById('btnCleanup');
if (data.hidden_empty > 0) {
infoEl.textContent = `⚠️ ${data.hidden_empty} session(s) vide(s)`;
btnCleanup.style.display = 'inline-block';
} else {
infoEl.textContent = '';
btnCleanup.style.display = 'none';
}
if (!data.sessions || data.sessions.length === 0) {
list.innerHTML = '<p style="text-align:center;color:#64748b;padding:40px;">Aucune session enregistrée</p>';
return;
}
let html = '';
data.sessions.forEach(session => {
const user = session.user?.label || session.user?.id || 'Inconnu';
const training = session.context?.training_label || 'Sans label';
const date = new Date(session.started_at).toLocaleString('fr-FR');
// Badge de statut de traitement
const state = session.processing_state || 'pending';
const stateConfig = {
'pending': { icon: '⏳', text: 'En attente', color: '#64748b', bg: '#334155' },
'processing': { icon: '⚙️', text: 'En cours...', color: '#f59e0b', bg: '#422006' },
'completed': { icon: '✅', text: 'Traité', color: '#22c55e', bg: '#052e16' },
'failed': { icon: '❌', text: 'Échec', color: '#ef4444', bg: '#450a0a' }
};
const sc = stateConfig[state] || stateConfig.pending;
const statsBadge = session.processing_stats ?
`<span style="margin-left:8px;font-size:11px;color:#94a3b8;">${session.processing_stats.targets_found || 0} cibles</span>` : '';
html += `
<div class="session-item">
<div class="session-info">
<h4>
${session.session_id}
<span style="margin-left:10px;padding:3px 8px;border-radius:4px;font-size:12px;background:${sc.bg};color:${sc.color};">
${sc.icon} ${sc.text}${statsBadge}
</span>
</h4>
<div class="meta">
👤 ${user} • 🏷️ ${training} • 📅 ${date}<br>
📸 ${session.screenshots_count} screenshots • 🎬 ${session.events_count} events • 💾 ${session.size_mb} MB
</div>
</div>
<div class="session-actions">
<button class="btn btn-primary btn-small" onclick="viewScreenshots('${session.session_id}')">📸 Screenshots</button>
<button class="btn btn-success btn-small" onclick="processSession('${session.session_id}')" ${state === 'processing' ? 'disabled' : ''}>
${state === 'processing' ? '⏳ En cours...' : '⚙️ Traiter'}
</button>
</div>
</div>
`;
});
list.innerHTML = html;
} catch (e) {
list.innerHTML = '<p style="color:#ef4444;">Erreur: ' + e.message + '</p>';
}
}
async function viewScreenshots(sessionId) {
const modal = document.getElementById('screenshotModal');
const gallery = document.getElementById('screenshotGallery');
const title = document.getElementById('modalTitle');
title.textContent = `Screenshots - ${sessionId}`;
gallery.innerHTML = '<div class="loading"><div class="spinner"></div>Chargement...</div>';
modal.style.display = 'flex';
try {
const data = await fetchJSON(`/api/agent/sessions/${sessionId}`);
if (!data.screenshots || data.screenshots.length === 0) {
gallery.innerHTML = '<p style="color:#64748b;">Aucun screenshot disponible</p>';
return;
}
let html = '';
data.screenshots.forEach(ss => {
html += `<img src="${ss.url}" alt="${ss.filename}" onclick="window.open('${ss.url}', '_blank')">`;
});
gallery.innerHTML = html;
} catch (e) {
gallery.innerHTML = '<p style="color:#ef4444;">Erreur: ' + e.message + '</p>';
}
}
function closeModal() {
document.getElementById('screenshotModal').style.display = 'none';
}
async function cleanupEmptySessions() {
const infoEl = document.getElementById('emptySessionsInfo');
const count = infoEl.textContent.match(/(\d+)/)?.[1] || '?';
if (!confirm(`Supprimer définitivement ${count} session(s) vide(s) (sans screenshots) ?`)) return;
const btn = document.getElementById('btnCleanup');
btn.disabled = true;
btn.textContent = '⏳ Suppression...';
try {
const data = await fetchJSON('/api/agent/sessions/cleanup-empty', { method: 'POST' });
if (data.success) {
alert(`${data.deleted_count} session(s) supprimée(s)`);
refreshSessions();
} else {
alert('❌ Erreur: ' + (data.error || 'Inconnue'));
}
} catch (e) {
alert('❌ Erreur: ' + e.message);
} finally {
btn.disabled = false;
btn.textContent = '🗑️ Supprimer vides';
}
}
async function processSession(sessionId) {
if (!confirm(`Lancer le traitement de ${sessionId}?`)) return;
try {
const data = await fetchJSON(`/api/agent/sessions/${sessionId}/process`, { method: 'POST' });
if (data.error) {
alert('Erreur: ' + data.error);
} else {
alert('Traitement lancé! Voir les logs pour suivre la progression.');
}
} catch (e) {
alert('Erreur: ' + e.message);
}
}
// Performance
async function refreshPerformance() {
try {
const data = await fetchJSON('/api/system/performance');
const faiss = data.faiss || {};
// Update FAISS Panel
const totalVectors = faiss.total_vectors || faiss.total_embeddings || 0;
const hasIndex = totalVectors > 0 || faiss.dimensions;
// Status indicator
const faissStatusEl = document.getElementById('faissStatus');
if (faiss.error) {
faissStatusEl.textContent = '❌';
faissStatusEl.title = faiss.error;
} else if (faiss.status === 'index_not_found') {
faissStatusEl.textContent = '⚠️';
faissStatusEl.title = 'Index non trouvé';
} else if (hasIndex) {
faissStatusEl.textContent = '✅';
faissStatusEl.title = 'Index actif';
} else {
faissStatusEl.textContent = '⏳';
faissStatusEl.title = 'En attente';
}
// FAISS metrics
document.getElementById('faissVectors').textContent = totalVectors.toLocaleString();
document.getElementById('faissDimensions').textContent = faiss.dimensions || '-';
document.getElementById('faissIndexType').textContent = faiss.index_type || 'Flat';
// FAISS details with tooltips
const details = [];
if (faiss.metric) details.push(`Métrique: ${faiss.metric}`);
if (faiss.use_gpu !== undefined) details.push(`GPU: ${faiss.use_gpu ? 'Oui' : 'Non'}`);
if (faiss.is_trained !== undefined) details.push(`Entraîné: ${faiss.is_trained ? 'Oui' : 'Non'}`);
if (faiss.nlist) details.push(`nlist: ${faiss.nlist}`);
if (faiss.nprobe) details.push(`nprobe: ${faiss.nprobe}`);
if (faiss.metadata_count) details.push(`Métadonnées: ${faiss.metadata_count}`);
document.getElementById('faissDetails').textContent = details.length > 0 ?
details.join(' • ') : (faiss.error || faiss.status || 'Aucune info disponible');
// Recommendations contextuelles
const recoEl = document.getElementById('faissRecommendations');
const recommendations = [];
if (totalVectors > 10000 && (faiss.index_type === 'Flat' || !faiss.index_type)) {
recommendations.push('💡 Passez à un index IVF pour de meilleures performances (> 10k vecteurs)');
}
if (!faiss.use_gpu && totalVectors > 50000) {
recommendations.push('🚀 Activez le GPU pour accélérer les recherches (> 50k vecteurs)');
}
if (faiss.status === 'index_not_found') {
recommendations.push('📝 Traitez des sessions pour créer l\'index FAISS');
}
if (recommendations.length > 0) {
recoEl.innerHTML = recommendations.join('<br>');
recoEl.style.display = 'block';
} else {
recoEl.style.display = 'none';
}
// Update simple stats
document.getElementById('perfEmbeddings').textContent = totalVectors;
const hits = data.embedding_cache?.hits || 0;
const misses = data.embedding_cache?.misses || 0;
const hitRate = hits + misses > 0 ? ((hits / (hits + misses)) * 100).toFixed(1) : 0;
document.getElementById('perfCacheHits').textContent = hitRate + '%';
const avgTime = faiss.avg_search_time_ms || data.metrics?.avg_search_time_ms || 0;
document.getElementById('perfAvgTime').textContent = avgTime.toFixed(2) + 'ms';
// Update charts
updatePerformanceCharts(data);
} catch (e) {
console.error('Performance error:', e);
document.getElementById('faissStatus').textContent = '❌';
document.getElementById('faissDetails').textContent = 'Erreur: ' + e.message;
document.getElementById('perfEmbeddings').textContent = '!';
document.getElementById('perfCacheHits').textContent = '!';
document.getElementById('perfAvgTime').textContent = '!';
}
}
// Test FAISS Index
async function testFaissIndex() {
const btn = document.getElementById('btnTestFaiss');
const resultsEl = document.getElementById('faissTestResults');
btn.disabled = true;
btn.textContent = '⏳ Test en cours...';
resultsEl.style.display = 'block';
resultsEl.innerHTML = '<div style="text-align:center;color:#94a3b8;">🔄 Exécution du test...</div>';
try {
const data = await fetchJSON('/api/system/faiss/test', { method: 'POST' });
if (data.success) {
let html = `
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;">
<span style="font-size:16px;font-weight:bold;color:#22c55e;">✅ Test réussi</span>
<span style="color:#64748b;font-size:12px;">${new Date().toLocaleTimeString('fr-FR')}</span>
</div>
<div class="grid grid-3" style="gap:15px;margin-bottom:15px;">
<div style="text-align:center;padding:10px;background:#1e293b;border-radius:8px;">
<div style="font-size:20px;font-weight:bold;color:#3b82f6;">${data.load_time_ms}ms</div>
<div style="font-size:11px;color:#64748b;">Chargement</div>
</div>
<div style="text-align:center;padding:10px;background:#1e293b;border-radius:8px;">
<div style="font-size:20px;font-weight:bold;color:#22c55e;">${data.search_time_ms}ms</div>
<div style="font-size:11px;color:#64748b;">Recherche</div>
</div>
<div style="text-align:center;padding:10px;background:#1e293b;border-radius:8px;">
<div style="font-size:20px;font-weight:bold;color:#8b5cf6;">${data.results_count}</div>
<div style="font-size:11px;color:#64748b;">Résultats</div>
</div>
</div>
`;
if (data.results && data.results.length > 0) {
html += `<div style="font-size:13px;color:#94a3b8;margin-bottom:8px;">Top résultats (vecteur aléatoire) :</div>`;
html += `<table style="width:100%;font-size:12px;color:#e2e8f0;">
<tr style="color:#64748b;"><th>#</th><th>Score</th><th>Target ID</th><th>Session</th></tr>`;
data.results.forEach(r => {
html += `<tr>
<td>${r.rank}</td>
<td>${r.score}</td>
<td>${r.target_id}</td>
<td>${r.session_id}...</td>
</tr>`;
});
html += `</table>`;
}
if (data.recommendations && data.recommendations.length > 0) {
html += `<div style="margin-top:15px;padding:10px;background:#422006;border-radius:8px;color:#fbbf24;font-size:13px;">
💡 ${data.recommendations.join('<br>💡 ')}
</div>`;
}
resultsEl.innerHTML = html;
} else {
resultsEl.innerHTML = `
<div style="color:#ef4444;font-weight:bold;">❌ ${data.error}</div>
${data.recommendation ? `<div style="margin-top:10px;color:#fbbf24;">💡 ${data.recommendation}</div>` : ''}
`;
}
} catch (e) {
resultsEl.innerHTML = `<div style="color:#ef4444;">❌ Erreur: ${e.message}</div>`;
} finally {
btn.disabled = false;
btn.textContent = '🧪 Tester l\'index';
}
}
// Track FAISS search history for chart
let faissSearchHistory = [];
const MAX_HISTORY = 20;
function updatePerformanceCharts(data) {
const faiss = data.faiss || {};
// Add current search time to history (if available)
const searchTime = faiss.avg_search_time_ms || data.metrics?.avg_search_time_ms || 0;
if (searchTime > 0 || faissSearchHistory.length > 0) {
faissSearchHistory.push({
time: new Date().toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }),
value: searchTime
});
if (faissSearchHistory.length > MAX_HISTORY) {
faissSearchHistory.shift();
}
}
// FAISS Chart - Search time history
if (!faissChart) {
const ctx = document.getElementById('faissChart').getContext('2d');
faissChart = new Chart(ctx, {
type: 'line',
data: {
labels: faissSearchHistory.map(h => h.time),
datasets: [{
label: 'Temps de recherche (ms)',
data: faissSearchHistory.map(h => h.value),
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
plugins: { legend: { labels: { color: '#94a3b8' } } },
scales: {
x: { ticks: { color: '#64748b' }, grid: { color: '#334155' } },
y: {
ticks: { color: '#64748b' },
grid: { color: '#334155' },
beginAtZero: true,
title: { display: true, text: 'ms', color: '#64748b' }
}
}
}
});
} else {
faissChart.data.labels = faissSearchHistory.map(h => h.time);
faissChart.data.datasets[0].data = faissSearchHistory.map(h => h.value);
faissChart.update('none');
}
// Cache Chart - Doughnut
const hits = data.embedding_cache?.hits || 0;
const misses = data.embedding_cache?.misses || 0;
if (!cacheChart) {
const ctx = document.getElementById('cacheChart').getContext('2d');
cacheChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['Hits', 'Misses'],
datasets: [{
data: [hits, misses],
backgroundColor: ['#22c55e', '#ef4444']
}]
},
options: {
responsive: true,
plugins: {
legend: { labels: { color: '#94a3b8' } },
tooltip: {
callbacks: {
label: (ctx) => {
const total = hits + misses;
const pct = total > 0 ? ((ctx.raw / total) * 100).toFixed(1) : 0;
return `${ctx.label}: ${ctx.raw} (${pct}%)`;
}
}
}
}
}
});
} else {
cacheChart.data.datasets[0].data = [hits, misses];
cacheChart.update('none');
}
}
// Tests
async function refreshTests() {
const list = document.getElementById('testList');
list.innerHTML = '<div class="loading"><div class="spinner"></div>Chargement...</div>';
try {
const data = await fetchJSON('/api/tests');
let html = '';
(data.tests || []).forEach(test => {
html += `
<div class="test-item" onclick="runTest('${test.path}')">
<div>
<span style="font-weight:500;">${test.name}</span>
<span class="test-type ${test.type}">${test.type}</span>
</div>
<button class="btn btn-primary btn-small">▶️</button>
</div>
`;
});
list.innerHTML = html || '<p style="text-align:center;color:#64748b;">Aucun test</p>';
} catch (e) {
list.innerHTML = '<p style="color:#ef4444;">Erreur: ' + e.message + '</p>';
}
}
async function runTest(testPath) {
const output = document.getElementById('testOutput');
output.textContent = '⏳ Exécution en cours...\n';
try {
const data = await fetchJSON('/api/tests/run', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ test_path: testPath })
});
const status = data.success ? '✅ RÉUSSI' : '❌ ÉCHOUÉ';
output.textContent = `Test: ${testPath}\nStatut: ${status}\n\n${data.stdout || ''}${data.stderr ? '\n\nErreurs:\n' + data.stderr : ''}`;
} catch (e) {
output.textContent = '❌ Erreur: ' + e.message;
}
}
async function runAllTests(type) {
const output = document.getElementById('testOutput');
output.textContent = `⏳ Exécution des tests ${type}...\n`;
try {
const data = await fetchJSON('/api/tests/run-all', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ type })
});
const status = data.success ? '✅ TOUS RÉUSSIS' : '❌ ÉCHECS';
output.textContent = `Tests ${type}\nStatut: ${status}\n\n${data.stdout || ''}`;
} catch (e) {
output.textContent = '❌ Erreur: ' + e.message;
}
}
// Chains
async function refreshChains() {
const list = document.getElementById('chainList');
list.innerHTML = '<div class="loading"><div class="spinner"></div>Chargement...</div>';
try {
const data = await fetchJSON('/api/chains');
if (!data.chains || data.chains.length === 0) {
list.innerHTML = '<p style="text-align:center;color:#64748b;padding:40px;">Aucune chaîne configurée</p>';
return;
}
let html = '';
data.chains.forEach(chain => {
const statusColor = chain.status === 'active' ? '#22c55e' : '#64748b';
html += `
<div class="workflow-card">
<h3>${chain.name}</h3>
<div class="meta">
${chain.workflows.length} workflows •
Taux de succès: ${chain.success_rate.toFixed(1)}%
</div>
<div style="margin:10px 0;color:#94a3b8;font-size:13px;">
${chain.workflows.join(' → ')}
</div>
<div style="margin-top:15px;">
<button class="btn btn-success btn-small" onclick="executeChain('${chain.chain_id}')">▶️ Exécuter</button>
</div>
</div>
`;
});
list.innerHTML = html;
} catch (e) {
list.innerHTML = '<p style="color:#ef4444;">Erreur: ' + e.message + '</p>';
}
}
async function executeChain(chainId) {
if (!confirm('Lancer l\'exécution de cette chaîne?')) return;
try {
const data = await fetchJSON(`/api/chains/${chainId}/execute`, { method: 'POST' });
if (data.error) {
alert('Erreur: ' + data.error);
} else {
alert(`Chaîne lancée! Durée: ${data.result?.duration?.toFixed(2) || '?'}s`);
refreshChains();
}
} catch (e) {
alert('Erreur: ' + e.message);
}
}
function showCreateChainModal() {
alert('Création de chaîne non implémentée dans cette version');
}
// Triggers
async function refreshTriggers() {
const list = document.getElementById('triggerList');
list.innerHTML = '<div class="loading"><div class="spinner"></div>Chargement...</div>';
try {
const data = await fetchJSON('/api/triggers');
if (!data.triggers || data.triggers.length === 0) {
list.innerHTML = '<p style="text-align:center;color:#64748b;padding:40px;">Aucun déclencheur configuré</p>';
return;
}
let html = '';
data.triggers.forEach(trigger => {
const statusColor = trigger.enabled ? '#22c55e' : '#ef4444';
const statusText = trigger.enabled ? 'Activé' : 'Désactivé';
html += `
<div class="workflow-card">
<h3>${trigger.trigger_type.toUpperCase()}</h3>
<div class="meta">
Workflow: ${trigger.workflow_id}<br>
Déclenchements: ${trigger.fire_count}
</div>
<div style="margin:10px 0;color:#94a3b8;font-size:13px;">
Config: ${JSON.stringify(trigger.config)}
</div>
<div style="margin-top:15px;">
<button class="btn ${trigger.enabled ? 'btn-warning' : 'btn-success'} btn-small"
onclick="toggleTrigger('${trigger.trigger_id}')">
${trigger.enabled ? '⏸️ Désactiver' : '▶️ Activer'}
</button>
</div>
</div>
`;
});
list.innerHTML = html;
} catch (e) {
list.innerHTML = '<p style="color:#ef4444;">Erreur: ' + e.message + '</p>';
}
}
async function toggleTrigger(triggerId) {
try {
const data = await fetchJSON(`/api/triggers/${triggerId}/toggle`, { method: 'POST' });
if (data.error) {
alert('Erreur: ' + data.error);
} else {
refreshTriggers();
}
} catch (e) {
alert('Erreur: ' + e.message);
}
}
function showCreateTriggerModal() {
alert('Création de trigger non implémentée dans cette version');
}
// Automation
async function refreshAutomationStatus() {
try {
const data = await fetchJSON('/api/automation/status');
document.getElementById('autoStatus').textContent = data.running ? '🟢 Actif' : '🔴 Arrêté';
document.getElementById('autoStatus').style.color = data.running ? '#22c55e' : '#ef4444';
document.getElementById('autoTriggers').textContent = data.active_triggers || 0;
document.getElementById('autoInterval').textContent = (data.check_interval || 0) + 's';
// Mettre à jour le bouton
const btn = document.getElementById('automationToggle');
if (data.running) {
btn.textContent = '⏸️ Arrêter';
btn.className = 'btn btn-warning btn-small';
} else {
btn.textContent = '▶️ Démarrer';
btn.className = 'btn btn-success btn-small';
}
} catch (e) {
console.error('Error refreshing automation status:', e);
document.getElementById('autoStatus').textContent = '❌ Erreur';
document.getElementById('autoStatus').style.color = '#ef4444';
}
}
async function toggleAutomation() {
try {
const statusData = await fetchJSON('/api/automation/status');
const endpoint = statusData.running ? '/api/automation/stop' : '/api/automation/start';
const data = await fetchJSON(endpoint, { method: 'POST' });
if (data.error) {
alert('Erreur: ' + data.error);
} else {
refreshAutomationStatus();
}
} catch (e) {
alert('Erreur: ' + e.message);
}
}
// Metrics
async function refreshMetrics() {
const output = document.getElementById('metricsOutput');
output.textContent = '⏳ Chargement des métriques...\n';
try {
const response = await fetch('/metrics');
if (!response.ok) {
throw new Error(`Serveur erreur ${response.status}`);
}
const text = await response.text();
output.textContent = text || 'Aucune métrique disponible';
} catch (e) {
output.textContent = '❌ Erreur: ' + e.message;
}
}
// Logs
async function refreshLogs() {
const output = document.getElementById('logsOutput');
output.textContent = '⏳ Chargement des logs...\n';
try {
const data = await fetchJSON('/api/logs');
if (!data.logs || data.logs.length === 0) {
output.textContent = 'Aucun log disponible';
return;
}
let text = '';
data.logs.forEach(log => {
text += `[${log.file}] ${log.message}\n`;
});
output.textContent = text;
} catch (e) {
output.textContent = '❌ Erreur: ' + e.message;
}
}
async function downloadLogs() {
try {
window.location.href = '/api/logs/download';
} catch (e) {
alert('Erreur: ' + e.message);
}
}
// Initialize overview chart
function initOverviewChart() {
const ctx = document.getElementById('perfChart').getContext('2d');
perfChart = new Chart(ctx, {
type: 'bar',
data: {
labels: ['Sessions', 'Workflows', 'Tests', 'Embeddings'],
datasets: [{
label: 'Compteurs',
data: [0, 0, 0, 0],
backgroundColor: ['#3b82f6', '#8b5cf6', '#22c55e', '#f59e0b']
}]
},
options: {
responsive: true,
plugins: { legend: { display: false } },
scales: {
x: { ticks: { color: '#64748b' }, grid: { color: '#334155' } },
y: { ticks: { color: '#64748b' }, grid: { color: '#334155' } }
}
}
});
}
// =====================================================
// Services Management (Panneau de contrôle pour démos)
// =====================================================
let servicesData = [];
async function refreshServices() {
const grid = document.getElementById('servicesGrid');
const quickLinks = document.getElementById('quickLinks');
grid.innerHTML = '<div class="loading"><div class="spinner"></div>Chargement des services...</div>';
try {
const data = await fetchJSON('/api/services');
servicesData = data.services || [];
let gridHtml = '';
let linksHtml = '';
servicesData.forEach(service => {
const isRunning = service.status === 'running';
const statusClass = isRunning ? 'running' : 'stopped';
const statusText = isRunning ? '🟢 En ligne' : '🔴 Arrêté';
gridHtml += `
<div class="service-card ${statusClass}" id="service-${service.service_id}">
<div class="status-indicator ${statusClass}"></div>
<div class="service-icon">${service.icon}</div>
<h3>${service.name}</h3>
<div class="description">${service.description}</div>
<div class="port-info">Port: ${service.port} ${statusText}</div>
<div class="actions">
${service.can_start ? `
<button class="btn ${isRunning ? 'btn-warning' : 'btn-success'}"
onclick="${isRunning ? `stopService('${service.service_id}')` : `startService('${service.service_id}')`}"
${!service.can_stop && isRunning ? 'disabled' : ''}>
${isRunning ? '⏹️ Arrêter' : '▶️ Démarrer'}
</button>
${isRunning && service.can_stop ? `
<button class="btn btn-secondary" onclick="restartService('${service.service_id}')">
🔄 Redémarrer
</button>
` : ''}
` : `
<button class="btn btn-secondary" disabled>
${isRunning ? '✅ Actif (ce service)' : '⚪ Non gérable'}
</button>
`}
${isRunning ? `
<button class="btn btn-primary" onclick="openService('${service.url}')">
🌐 Ouvrir
</button>
` : ''}
</div>
</div>
`;
// Quick links
linksHtml += `
<a href="${service.url}" target="_blank" class="quick-link" onclick="event.preventDefault(); openService('${service.url}');">
<span class="icon">${service.icon}</span>
<div class="info">
<div class="name">${service.name}</div>
<div class="url">${service.url}</div>
</div>
<span class="status-badge ${statusClass}">${isRunning ? 'EN LIGNE' : 'ARRÊTÉ'}</span>
</a>
`;
});
grid.innerHTML = gridHtml;
quickLinks.innerHTML = linksHtml;
} catch (e) {
grid.innerHTML = `<p style="color:#ef4444;padding:20px;">❌ Erreur: ${e.message}</p>`;
}
}
async function startService(serviceId) {
const btn = event.target;
btn.disabled = true;
btn.innerHTML = '⏳ Démarrage...';
try {
const data = await fetchJSON(`/api/services/${serviceId}/start`, { method: 'POST' });
if (data.error) {
alert('❌ Erreur: ' + data.error);
} else {
// Notification de succès
showNotification(`${data.message || 'Service démarré'}`, 'success');
}
// Attendre un peu et rafraîchir
setTimeout(refreshServices, 1500);
} catch (e) {
alert('❌ Erreur: ' + e.message);
btn.disabled = false;
btn.innerHTML = '▶️ Démarrer';
}
}
async function stopService(serviceId) {
if (!confirm('Arrêter ce service ?')) return;
const btn = event.target;
btn.disabled = true;
btn.innerHTML = '⏳ Arrêt...';
try {
const data = await fetchJSON(`/api/services/${serviceId}/stop`, { method: 'POST' });
if (data.error) {
alert('❌ Erreur: ' + data.error);
} else {
showNotification(`⏹️ ${data.message || 'Service arrêté'}`, 'warning');
}
setTimeout(refreshServices, 1000);
} catch (e) {
alert('❌ Erreur: ' + e.message);
btn.disabled = false;
}
}
async function restartService(serviceId) {
const btn = event.target;
btn.disabled = true;
btn.innerHTML = '⏳ Redémarrage...';
try {
const data = await fetchJSON(`/api/services/${serviceId}/restart`, { method: 'POST' });
if (data.error) {
alert('❌ Erreur: ' + data.error);
} else {
showNotification(`🔄 Service redémarré`, 'success');
}
setTimeout(refreshServices, 2000);
} catch (e) {
alert('❌ Erreur: ' + e.message);
btn.disabled = false;
}
}
async function startAllServices() {
if (!confirm('Démarrer tous les services ?')) return;
showNotification('⏳ Démarrage de tous les services...', 'info');
try {
const data = await fetchJSON('/api/services/start-all', { method: 'POST' });
let successCount = 0;
let failCount = 0;
for (const [id, result] of Object.entries(data.results || {})) {
if (result.status === 'running' || result.status === 'already_running') {
successCount++;
} else {
failCount++;
}
}
showNotification(`${successCount} service(s) démarré(s)${failCount > 0 ? `, ${failCount} échec(s)` : ''}`, 'success');
setTimeout(refreshServices, 1000);
} catch (e) {
alert('❌ Erreur: ' + e.message);
}
}
async function stopAllServices() {
if (!confirm('Arrêter tous les services (sauf ce dashboard) ?')) return;
showNotification('⏳ Arrêt de tous les services...', 'warning');
try {
const data = await fetchJSON('/api/services/stop-all', { method: 'POST' });
showNotification('⏹️ Services arrêtés', 'warning');
setTimeout(refreshServices, 1000);
} catch (e) {
alert('❌ Erreur: ' + e.message);
}
}
function openService(url) {
window.open(url, '_blank');
}
// Notification system
function showNotification(message, type = 'info') {
// Remove existing notification
const existing = document.getElementById('notification');
if (existing) existing.remove();
const colors = {
success: { bg: '#052e16', border: '#22c55e', text: '#22c55e' },
warning: { bg: '#422006', border: '#f59e0b', text: '#f59e0b' },
error: { bg: '#450a0a', border: '#ef4444', text: '#ef4444' },
info: { bg: '#1e293b', border: '#3b82f6', text: '#3b82f6' }
};
const c = colors[type] || colors.info;
const div = document.createElement('div');
div.id = 'notification';
div.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 15px 25px;
background: ${c.bg};
border: 1px solid ${c.border};
border-radius: 10px;
color: ${c.text};
font-weight: 500;
z-index: 9999;
animation: slideIn 0.3s ease;
`;
div.textContent = message;
document.body.appendChild(div);
setTimeout(() => div.remove(), 4000);
}
// ============================================================
// SECTION: Sauvegardes / Backups
// ============================================================
async function refreshBackupStats() {
try {
const data = await fetchJSON('/api/backup/stats');
// 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);
}
}
async function downloadBackup(type) {
showNotification(`⬇️ Téléchargement ${type}...`, 'info');
try {
const response = await fetch(`/api/backup/${type}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `rpa_${type}_${new Date().toISOString().split('T')[0]}.zip`;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
showNotification(`✅ Backup ${type} téléchargé`, 'success');
} catch (e) {
showNotification(`❌ Erreur: ${e.message}`, 'error');
}
}
async function downloadFullBackup() {
showNotification('⬇️ Génération du backup complet...', 'info');
try {
const response = await fetch('/api/backup/full', { method: 'POST' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `rpa_full_backup_${new Date().toISOString().split('T')[0]}.zip`;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
showNotification('✅ Backup complet téléchargé', 'success');
} catch (e) {
showNotification(`❌ Erreur: ${e.message}`, 'error');
}
}
// ============================================================
// SECTION: Système / Version
// ============================================================
async function refreshSystemInfo() {
try {
// 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 — 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>${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
const update = await fetchJSON('/api/version/check-update');
if (update.update_available) {
document.getElementById('sysUpdateStatus').innerHTML = `<span style="color:#f59e0b;">⬆️ ${update.update?.version}</span>`;
} else {
document.getElementById('sysUpdateStatus').innerHTML = '<span style="color:#22c55e;">✅ À jour</span>';
}
// Version backups
await refreshVersionBackups();
} catch (e) {
console.error('Error loading system info:', e);
document.getElementById('systemInfoDetails').innerHTML = `<p style="color:#ef4444;">Erreur: ${e.message}</p>`;
}
}
async function refreshVersionBackups() {
try {
const data = await fetchJSON('/api/version/backups');
const list = document.getElementById('versionBackupsList');
if (!data.backups || data.backups.length === 0) {
list.innerHTML = '<p style="color:#64748b;text-align:center;padding:20px;">Aucun point de restauration</p>';
return;
}
list.innerHTML = data.backups.slice(0, 10).map(b => `
<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')} — v${b.version || '-'}</span>
</div>
<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) {
document.getElementById('versionBackupsList').innerHTML = `<p style="color:#ef4444;">Erreur: ${e.message}</p>`;
}
}
async function createVersionBackup() {
const label = prompt('Nom du point de restauration (optionnel):');
showNotification('💾 Création du point de restauration...', 'info');
try {
await fetchJSON('/api/version/create-backup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ label })
});
showNotification('✅ Point de restauration créé', 'success');
await refreshVersionBackups();
} catch (e) {
showNotification(`❌ Erreur: ${e.message}`, 'error');
}
}
async function restoreVersion(backupId) {
// 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() {
const input = document.getElementById('updateFileInput');
if (!input.files.length) return;
const file = input.files[0];
const formData = new FormData();
formData.append('file', file);
showNotification('📤 Upload du package...', 'info');
try {
const response = await fetch('/api/version/upload-update', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.error) throw new Error(data.error);
const infoDiv = document.getElementById('updatePackageInfo');
infoDiv.style.display = 'block';
infoDiv.innerHTML = `
<p style="color:#22c55e;margin-bottom:10px;">✅ Package uploadé</p>
<p style="color:#94a3b8;font-size:13px;">Version: ${data.manifest?.version || '-'}</p>
<p style="color:#94a3b8;font-size:13px;">${data.manifest?.description || ''}</p>
`;
showNotification('✅ Package uploadé avec succès', 'success');
await refreshSystemInfo();
} catch (e) {
showNotification(`❌ Erreur: ${e.message}`, 'error');
}
input.value = '';
}
// ============================================================
// SECTION: Correction Packs
// ============================================================
async function refreshCorrectionPacks() {
try {
// Essayer d'abord l'API VWB (port 5002)
let data;
try {
data = await fetchJSON('http://localhost:5002/api/correction-packs/stats');
} catch (e) {
// Fallback sur les stats de backup
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;
document.getElementById('statTotalCorrections').textContent = data.total_corrections || 0;
document.getElementById('statApplications').textContent = data.total_applications || 0;
document.getElementById('statSuccessRate').textContent = data.overall_success_rate ?
Math.round(data.overall_success_rate * 100) + '%' : '-';
// Charger la liste des packs
await loadCorrectionPacksList();
} catch (e) {
console.error('Error loading correction packs:', e);
}
}
async function loadCorrectionPacksList() {
const list = document.getElementById('correctionPacksList');
try {
let packs;
try {
const data = await fetchJSON('http://localhost:5002/api/correction-packs');
packs = data.packs || [];
} catch (e) {
packs = [];
}
if (packs.length === 0) {
list.innerHTML = `
<div style="text-align:center;padding:40px;color:#64748b;">
<div style="font-size:48px;margin-bottom:15px;">📦</div>
<p>Aucun pack de corrections</p>
<p style="font-size:12px;margin-top:10px;">Créez un pack pour capitaliser les corrections utilisateur</p>
</div>
`;
return;
}
list.innerHTML = packs.map(pack => `
<div class="correction-pack-item">
<div class="pack-main">
<div class="pack-icon">📦</div>
<div class="pack-details">
<h4>${pack.name || 'Pack sans nom'}</h4>
<p>${pack.description || 'Pas de description'}${pack.category || 'général'}</p>
</div>
</div>
<div class="pack-stats">
<div class="pack-stat">
<div class="value">${pack.corrections_count || 0}</div>
<div class="label">Corrections</div>
</div>
<div class="pack-stat">
<div class="value">${pack.applications_count || 0}</div>
<div class="label">Applications</div>
</div>
<div class="pack-stat">
<div class="value">${pack.success_rate ? Math.round(pack.success_rate * 100) + '%' : '-'}</div>
<div class="label">Succès</div>
</div>
</div>
<div class="pack-actions">
<button class="btn btn-primary btn-small" onclick="exportPack('${pack.id}')">⬇️</button>
<button class="btn btn-danger btn-small" onclick="deletePack('${pack.id}')">🗑️</button>
</div>
</div>
`).join('');
} catch (e) {
list.innerHTML = `<p style="color:#ef4444;padding:20px;">Erreur: ${e.message}</p>`;
}
}
function showCreatePackModal() {
document.getElementById('createPackModal').style.display = 'flex';
}
function closeCreatePackModal() {
document.getElementById('createPackModal').style.display = 'none';
document.getElementById('packName').value = '';
document.getElementById('packDescription').value = '';
document.getElementById('packCategory').value = '';
}
async function createCorrectionPack() {
const name = document.getElementById('packName').value.trim();
if (!name) {
showNotification('❌ Le nom est requis', 'error');
return;
}
const description = document.getElementById('packDescription').value.trim();
const category = document.getElementById('packCategory').value.trim();
showNotification('📦 Création du pack...', 'info');
try {
await fetchJSON('http://localhost:5002/api/correction-packs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, description, category })
});
showNotification('✅ Pack créé avec succès', 'success');
closeCreatePackModal();
await refreshCorrectionPacks();
} catch (e) {
showNotification(`❌ Erreur: ${e.message}`, 'error');
}
}
async function exportPack(packId) {
showNotification('⬇️ Export du pack...', 'info');
try {
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();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `correction_pack_${packId}.json`;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
showNotification('✅ Pack exporté', 'success');
} catch (e) {
showNotification(`❌ Erreur: ${e.message}`, 'error');
}
}
async function deletePack(packId) {
if (!confirm('Supprimer ce pack ? Cette action est irréversible.')) return;
showNotification('🗑️ Suppression...', 'warning');
try {
await fetchJSON(`http://localhost:5002/api/correction-packs/${packId}`, { method: 'DELETE' });
showNotification('✅ Pack supprimé', 'success');
await refreshCorrectionPacks();
} catch (e) {
showNotification(`❌ Erreur: ${e.message}`, 'error');
}
}
// ============================================================
// SECTION: Apprentissage / Learning
// ============================================================
let corpusChart, healingChart;
async function refreshLearningStats() {
try {
// Stats depuis l'API de performance
const perf = await fetchJSON('/api/system/performance');
// Stats FAISS — la clé est total_vectors ou total_embeddings
const faiss = perf.faiss || {};
document.getElementById('statCorpusSize').textContent = faiss.total_vectors || faiss.total_embeddings || 0;
// Stats sessions
const status = await fetchJSON('/api/system/status');
document.getElementById('statTrainedSessions').textContent = status.sessions_count || 0;
// Stats self-healing depuis correction packs
let healingRate = '-';
let learningRate = '-';
try {
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 ?
'+' + corrections.total_corrections : '-';
} catch (e) {}
document.getElementById('statHealingSuccess').textContent = healingRate;
document.getElementById('statLearningRate').textContent = learningRate;
// Action type stats
await loadActionTypeStats();
// Top corrections
await loadTopCorrections();
// Initialize charts
initLearningCharts();
} catch (e) {
console.error('Error loading learning stats:', e);
}
}
async function loadActionTypeStats() {
const div = document.getElementById('actionTypeStats');
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() {
const div = document.getElementById('topCorrections');
try {
let corrections = [];
try {
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) {
corrections.push({
name: pack.name,
applications: pack.applications_count,
success_rate: pack.success_rate || 0
});
}
}
} catch (e) {}
if (corrections.length === 0) {
div.innerHTML = '<p style="color:#64748b;text-align:center;">Aucune correction appliquée</p>';
return;
}
corrections.sort((a, b) => b.applications - a.applications);
div.innerHTML = corrections.slice(0, 5).map((c, i) => `
<div class="stat-bar">
<span class="stat-bar-label">${i + 1}. ${c.name}</span>
<span class="stat-bar-value">${c.applications} apps (${Math.round(c.success_rate * 100)}%)</span>
</div>
`).join('');
} catch (e) {
div.innerHTML = `<p style="color:#ef4444;">Erreur: ${e.message}</p>`;
}
}
function initLearningCharts() {
// Corpus evolution chart
const corpusCtx = document.getElementById('corpusChart');
if (corpusCtx && !corpusChart) {
corpusChart = new Chart(corpusCtx, {
type: 'line',
data: {
labels: ['Sem -4', 'Sem -3', 'Sem -2', 'Sem -1', 'Cette sem'],
datasets: [{
label: 'Taille corpus (vecteurs)',
data: [100, 180, 290, 420, 512],
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
plugins: { legend: { labels: { color: '#94a3b8' } } },
scales: {
x: { ticks: { color: '#64748b' }, grid: { color: '#334155' } },
y: { ticks: { color: '#64748b' }, grid: { color: '#334155' } }
}
}
});
}
// Healing performance chart
const healingCtx = document.getElementById('healingChart');
if (healingCtx && !healingChart) {
healingChart = new Chart(healingCtx, {
type: 'bar',
data: {
labels: ['Succès', 'Échec', 'Non tenté'],
datasets: [{
label: 'Self-healing',
data: [75, 15, 10],
backgroundColor: ['#22c55e', '#ef4444', '#64748b']
}]
},
options: {
responsive: true,
plugins: { legend: { labels: { color: '#94a3b8' } } },
scales: {
x: { ticks: { color: '#64748b' }, grid: { color: '#334155' } },
y: { ticks: { color: '#64748b' }, grid: { color: '#334155' } }
}
}
});
}
}
// ============================================================
// SECTION: Configuration
// ============================================================
let currentConfig = {};
async function refreshConfig() {
try {
const data = await fetchJSON('/api/config');
if (data.success) {
currentConfig = data.config;
populateConfigForm(data.config);
showNotification('Configuration chargee', 'success');
}
} catch (e) {
console.error('Error loading config:', e);
showNotification('Erreur chargement config: ' + e.message, 'error');
}
}
function populateConfigForm(config) {
// Services
if (config.services) {
for (const [key, svc] of Object.entries(config.services)) {
const hostEl = document.getElementById(`cfg_${key}_host`);
const portEl = document.getElementById(`cfg_${key}_port`);
if (hostEl) hostEl.value = svc.host || 'localhost';
if (portEl) portEl.value = svc.port || '';
}
}
// LLM
if (config.llm) {
document.getElementById('cfg_llm_provider').value = config.llm.provider || 'ollama';
document.getElementById('cfg_llm_base_url').value = config.llm.base_url || 'http://localhost:11434';
document.getElementById('cfg_llm_temperature').value = config.llm.temperature || 0.7;
document.getElementById('tempValue').textContent = config.llm.temperature || 0.7;
document.getElementById('cfg_llm_max_tokens').value = config.llm.max_tokens || 2048;
// Charger les modeles Ollama
refreshOllamaModels();
}
// Detection (OWL-v2 legacy) — section UI retiree, config preservee telle quelle
// Database
if (config.database) {
document.getElementById('cfg_database_type').value = config.database.type || 'sqlite';
document.getElementById('cfg_database_path').value = config.database.path || 'data/training/workflows.db';
document.getElementById('cfg_database_backup_enabled').checked = config.database.backup_enabled !== false;
document.getElementById('cfg_database_backup_interval').value = config.database.backup_interval_hours || 24;
}
// Security
if (config.security) {
document.getElementById('cfg_security_encryption').checked = config.security.enable_encryption !== false;
document.getElementById('cfg_security_auth').checked = config.security.require_authentication === true;
document.getElementById('cfg_security_timeout').value = config.security.session_timeout_minutes || 60;
document.getElementById('cfg_security_origins').value = (config.security.allowed_origins || []).join(',');
}
// Logging
if (config.logging) {
document.getElementById('cfg_logging_level').value = config.logging.level || 'INFO';
document.getElementById('cfg_logging_file').value = config.logging.file_path || 'logs/rpa_vision.log';
document.getElementById('cfg_logging_max_size').value = config.logging.max_size_mb || 50;
document.getElementById('cfg_logging_backup_count').value = config.logging.backup_count || 5;
}
}
function collectConfigFromForm() {
const config = {
version: currentConfig.version || '1.0.0',
services: {
vwb_backend: {
host: document.getElementById('cfg_vwb_backend_host').value || 'localhost',
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) || 3002,
description: 'Visual Workflow Builder - Interface React'
},
web_dashboard: {
host: 'localhost',
port: 5001,
description: 'Dashboard de monitoring RPA'
},
agent_chat: {
host: document.getElementById('cfg_agent_chat_host').value || 'localhost',
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,
description: 'API d\'upload de sessions'
}
},
llm: {
provider: document.getElementById('cfg_llm_provider').value,
base_url: document.getElementById('cfg_llm_base_url').value,
model: document.getElementById('cfg_llm_model').value,
temperature: parseFloat(document.getElementById('cfg_llm_temperature').value),
max_tokens: parseInt(document.getElementById('cfg_llm_max_tokens').value),
description: 'Modele LLM pour le parsing de commandes'
},
vlm: {
provider: document.getElementById('cfg_llm_provider').value,
base_url: document.getElementById('cfg_llm_base_url').value,
model: document.getElementById('cfg_vlm_model').value,
description: 'Modele VLM pour l\'analyse visuelle'
},
// Detection: section UI retiree (OWL-v2 remplace par pipeline VLM).
// On preserve la config existante pour le fallback eventuel.
detection: currentConfig.detection || {
owl_model: 'google/owlv2-base-patch16-ensemble',
confidence_threshold: 0.3,
nms_threshold: 0.3,
use_gpu: true,
description: 'Configuration legacy du detecteur visuel OWL-v2 (fallback)'
},
embedding: currentConfig.embedding || {
model: 'clip',
dimension: 512,
use_gpu: true,
cache_size: 10000,
description: 'Configuration des embeddings visuels'
},
database: {
type: document.getElementById('cfg_database_type').value,
path: document.getElementById('cfg_database_path').value,
backup_enabled: document.getElementById('cfg_database_backup_enabled').checked,
backup_interval_hours: parseInt(document.getElementById('cfg_database_backup_interval').value),
description: 'Base de donnees des workflows'
},
faiss: currentConfig.faiss || {
index_type: 'Flat',
use_gpu: true,
nprobe: 10,
description: 'Configuration de l\'index FAISS'
},
security: {
enable_encryption: document.getElementById('cfg_security_encryption').checked,
session_timeout_minutes: parseInt(document.getElementById('cfg_security_timeout').value),
require_authentication: document.getElementById('cfg_security_auth').checked,
allowed_origins: document.getElementById('cfg_security_origins').value.split(',').map(s => s.trim()).filter(s => s),
description: 'Parametres de securite'
},
logging: {
level: document.getElementById('cfg_logging_level').value,
file_path: document.getElementById('cfg_logging_file').value,
max_size_mb: parseInt(document.getElementById('cfg_logging_max_size').value),
backup_count: parseInt(document.getElementById('cfg_logging_backup_count').value),
description: 'Configuration des logs'
}
};
return config;
}
async function saveConfig() {
try {
const config = collectConfigFromForm();
showNotification('Sauvegarde en cours...', 'info');
const response = await fetchJSON('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
if (response.success) {
currentConfig = config;
showNotification('Configuration sauvegardee avec succes', 'success');
} else {
showNotification('Erreur: ' + (response.error || 'Inconnu'), 'error');
}
} catch (e) {
showNotification('Erreur: ' + e.message, 'error');
}
}
async function refreshOllamaModels() {
try {
const data = await fetchJSON('/api/config/ollama-models');
const llmSelect = document.getElementById('cfg_llm_model');
const vlmSelect = document.getElementById('cfg_vlm_model');
if (data.success && data.models) {
const llmOptions = data.models.map(m => `<option value="${m}" ${m === currentConfig.llm?.model ? 'selected' : ''}>${m}</option>`).join('');
const vlmOptions = data.models.map(m => `<option value="${m}" ${m === currentConfig.vlm?.model ? 'selected' : ''}>${m}</option>`).join('');
llmSelect.innerHTML = llmOptions || '<option value="">Aucun modele</option>';
vlmSelect.innerHTML = vlmOptions || '<option value="">Aucun modele</option>';
} else {
llmSelect.innerHTML = '<option value="">Ollama inaccessible</option>';
vlmSelect.innerHTML = '<option value="">Ollama inaccessible</option>';
}
} catch (e) {
console.error('Error loading Ollama models:', e);
document.getElementById('cfg_llm_model').innerHTML = '<option value="">Erreur chargement</option>';
document.getElementById('cfg_vlm_model').innerHTML = '<option value="">Erreur chargement</option>';
}
}
async function testOllamaConnection() {
const baseUrl = document.getElementById('cfg_llm_base_url').value;
showNotification('Test connexion Ollama...', 'info');
try {
const response = await fetchJSON('/api/config/test-connection', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'ollama', base_url: baseUrl })
});
updateTestResults('Ollama', response.success, response.message || response.error);
if (response.success) {
showNotification('Connexion Ollama OK - ' + (response.models?.length || 0) + ' modeles', 'success');
await refreshOllamaModels();
} else {
showNotification('Erreur Ollama: ' + response.error, 'error');
}
} catch (e) {
updateTestResults('Ollama', false, e.message);
showNotification('Erreur: ' + e.message, 'error');
}
}
async function testDatabaseConnection() {
const path = document.getElementById('cfg_database_path').value;
showNotification('Test connexion base de donnees...', 'info');
try {
const response = await fetchJSON('/api/config/test-connection', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'database', path: path })
});
updateTestResults('Database', response.success, response.message || response.error);
if (response.success) {
showNotification('Base de donnees accessible', 'success');
} else {
showNotification('Erreur DB: ' + response.error, 'error');
}
} catch (e) {
updateTestResults('Database', false, e.message);
showNotification('Erreur: ' + e.message, 'error');
}
}
async function testConnection(type, serviceKey) {
const hostEl = document.getElementById(`cfg_${serviceKey}_host`);
const portEl = document.getElementById(`cfg_${serviceKey}_port`);
if (!hostEl || !portEl) return;
showNotification(`Test connexion ${serviceKey}...`, 'info');
try {
const response = await fetchJSON('/api/config/test-connection', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'service',
host: hostEl.value,
port: portEl.value
})
});
updateTestResults(serviceKey, response.success, response.message || response.error);
if (response.success) {
showNotification(`${serviceKey} accessible`, 'success');
} else {
showNotification(`${serviceKey} inaccessible: ` + response.error, 'error');
}
} catch (e) {
updateTestResults(serviceKey, false, e.message);
showNotification('Erreur: ' + e.message, 'error');
}
}
function updateTestResults(name, success, message) {
const container = document.getElementById('testResultsContent');
const existing = container.querySelector(`[data-test="${name}"]`);
const html = `
<div data-test="${name}" style="display:flex;align-items:center;gap:10px;padding:8px;border-radius:6px;margin-bottom:5px;background:${success ? 'rgba(34,197,94,0.1)' : 'rgba(239,68,68,0.1)'};">
<span style="color:${success ? '#22c55e' : '#ef4444'};">${success ? '✅' : '❌'}</span>
<span style="color:#e2e8f0;font-weight:500;">${name}</span>
<span style="color:#94a3b8;font-size:12px;">${message}</span>
</div>
`;
if (existing) {
existing.outerHTML = html;
} else {
if (container.querySelector('.loading') || container.textContent.includes('Cliquez')) {
container.innerHTML = html;
} else {
container.innerHTML += html;
}
}
}
async function exportConfig() {
try {
const response = await fetch('/api/config/export');
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `rpa_config_${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
showNotification('Configuration exportee', 'success');
} catch (e) {
showNotification('Erreur export: ' + e.message, 'error');
}
}
async function importConfig(input) {
if (!input.files.length) return;
const file = input.files[0];
showNotification('Import en cours...', 'info');
try {
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/config/import', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
showNotification('Configuration importee avec succes', 'success');
await refreshConfig();
} else {
showNotification('Erreur import: ' + data.error, 'error');
}
} catch (e) {
showNotification('Erreur import: ' + e.message, 'error');
}
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();
refreshServices(); // Charger les services en premier
refreshSystemStatus();
refreshWorkflows();
setInterval(refreshSystemStatus, 10000);
setInterval(refreshServices, 15000); // Rafraîchir les services toutes les 15s
// Charger les stats des sections secondaires (en arrière-plan)
setTimeout(() => {
refreshBackupStats();
refreshSystemInfo(); // Chargé dans l'onglet Services
}, 1000);
});
// Add CSS animation for notifications
const style = document.createElement('style');
style.textContent = '@keyframes slideIn { from { transform: translateX(100px); opacity: 0; } to { transform: translateX(0); opacity: 1; } }';
document.head.appendChild(style);
</script>
</body>
</html>