Refonte majeure du système Agent Chat et ajout de nombreux modules : - Chat unifié : suppression du dual Workflows/Agent Libre, tout passe par /api/chat avec résolution en 3 niveaux (workflow → geste → "montre-moi") - GestureCatalog : 38 raccourcis clavier universels Windows avec matching sémantique, substitution automatique dans les replays, et endpoint /api/gestures - Mode Copilot : exécution pas-à-pas des workflows avec validation humaine via WebSocket (approve/skip/abort) avant chaque action - Léa UI (agent_v0/lea_ui/) : interface PyQt5 pour Windows avec overlay transparent pour feedback visuel pendant le replay - Data Extraction (core/extraction/) : moteur d'extraction visuelle de données (OCR + VLM → SQLite), avec schémas YAML et export CSV/Excel - ReplayVerifier (agent_v0/server_v1/) : vérification post-action par comparaison de screenshots, avec logique de retry (max 3) - IntentParser durci : meilleur fallback regex, type GREETING, patterns améliorés - Dashboard : nouvelles pages gestures, streaming, extractions - Tests : 63 tests GestureCatalog, 47 tests extraction, corrections tests existants - Dépréciation : /api/agent/plan et /api/agent/execute retournent HTTP 410, suppression du code hardcodé _plan_to_replay_actions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
3379 lines
171 KiB
HTML
3379 lines
171 KiB
HTML
<!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>
|
||
|
||
<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()">×</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 ✅ Actif = Index chargé et fonctionnel ⚠️ Non trouvé = Aucun index créé ❌ 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 Chaque cible cliquée génère un vecteur 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 512 = CLIP ViT-B/32 (standard) 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 Flat = Recherche exhaustive (précis, lent sur gros volumes) 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()">×</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>
|
||
</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 Detection -->
|
||
<div class="card">
|
||
<h2><span class="icon">👁️</span> Detection Visuelle</h2>
|
||
<div id="configDetection" class="config-section">
|
||
<div class="config-item">
|
||
<label>Modele OWL</label>
|
||
<select id="cfg_detection_owl_model" class="config-input">
|
||
<option value="google/owlv2-base-patch16-ensemble">OWLv2 Base Ensemble</option>
|
||
<option value="google/owlv2-large-patch14-ensemble">OWLv2 Large Ensemble</option>
|
||
<option value="google/owlvit-base-patch32">OWLViT Base</option>
|
||
</select>
|
||
</div>
|
||
<div class="config-item">
|
||
<label>Seuil de confiance</label>
|
||
<input type="range" id="cfg_detection_confidence" min="0.1" max="0.9" step="0.05" value="0.3" class="config-input" oninput="document.getElementById('confValue').textContent = this.value">
|
||
<span id="confValue" style="color:#3b82f6;margin-left:10px;">0.3</span>
|
||
</div>
|
||
<div class="config-item">
|
||
<label>Seuil NMS</label>
|
||
<input type="range" id="cfg_detection_nms" min="0.1" max="0.9" step="0.05" value="0.3" class="config-input" oninput="document.getElementById('nmsValue').textContent = this.value">
|
||
<span id="nmsValue" style="color:#3b82f6;margin-left:10px;">0.3</span>
|
||
</div>
|
||
<div class="config-item">
|
||
<label>
|
||
<input type="checkbox" id="cfg_detection_use_gpu" checked>
|
||
Utiliser GPU (CUDA)
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 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>
|
||
</div>
|
||
|
||
<div class="grid grid-2">
|
||
<!-- 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>
|
||
</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();
|
||
}
|
||
|
||
// 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
|
||
if (config.detection) {
|
||
document.getElementById('cfg_detection_owl_model').value = config.detection.owl_model || 'google/owlv2-base-patch16-ensemble';
|
||
document.getElementById('cfg_detection_confidence').value = config.detection.confidence_threshold || 0.3;
|
||
document.getElementById('confValue').textContent = config.detection.confidence_threshold || 0.3;
|
||
document.getElementById('cfg_detection_nms').value = config.detection.nms_threshold || 0.3;
|
||
document.getElementById('nmsValue').textContent = config.detection.nms_threshold || 0.3;
|
||
document.getElementById('cfg_detection_use_gpu').checked = config.detection.use_gpu !== false;
|
||
}
|
||
|
||
// 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: {
|
||
owl_model: document.getElementById('cfg_detection_owl_model').value,
|
||
confidence_threshold: parseFloat(document.getElementById('cfg_detection_confidence').value),
|
||
nms_threshold: parseFloat(document.getElementById('cfg_detection_nms').value),
|
||
use_gpu: document.getElementById('cfg_detection_use_gpu').checked,
|
||
description: 'Configuration du detecteur visuel OWL-v2'
|
||
},
|
||
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>
|