Files
rpa_vision_v3/web_dashboard/templates/index.html
Dom 61b03bc147 feat(dashboard): Ajouter panneau de contrôle des services pour démos
- Ajouter API /api/services pour lister/démarrer/arrêter les services
- Ajouter nouvel onglet "Services" comme page d'accueil du dashboard
- Indicateurs visuels (vert=actif, rouge=arrêté) pour chaque service
- Boutons: Démarrer, Arrêter, Redémarrer, Ouvrir dans navigateur
- Support: Agent Chat, VWB Backend, VWB Frontend, Dashboard
- Actions groupées: Tout Démarrer / Tout Arrêter
- Notifications visuelles pour les actions
- Rafraîchissement auto toutes les 5 secondes

Services gérés:
- Agent Chat (LLM) - port 5002
- VWB Backend - port 5000
- VWB Frontend - port 3000
- Dashboard - port 5001

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 17:45:05 +01:00

1700 lines
83 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RPA Vision V3 - Dashboard</title>
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; }
.header { background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); color: white; padding: 20px 30px; display: flex; justify-content: space-between; align-items: center; }
.header h1 { font-size: 24px; display: flex; align-items: center; gap: 10px; }
.header .status { display: flex; align-items: center; gap: 8px; font-size: 14px; }
.status-dot { width: 10px; height: 10px; border-radius: 50%; background: #22c55e; animation: pulse 2s infinite; }
.status-dot.offline { background: #ef4444; animation: none; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
.tabs { background: #1e293b; border-bottom: 1px solid #334155; display: flex; padding: 0 20px; }
.tab { padding: 15px 25px; cursor: pointer; border-bottom: 3px solid transparent; transition: all 0.2s; color: #94a3b8; font-weight: 500; }
.tab:hover { color: #e2e8f0; background: #334155; }
.tab.active { border-bottom-color: #3b82f6; color: #3b82f6; }
.container { max-width: 1600px; margin: 0 auto; padding: 20px; }
.tab-content { display: none; }
.tab-content.active { display: block; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 20px; }
.grid-2 { grid-template-columns: 1fr 1fr; }
.grid-3 { grid-template-columns: 1fr 1fr 1fr; }
.grid-4 { grid-template-columns: repeat(4, 1fr); }
.card { background: #1e293b; border-radius: 12px; padding: 20px; border: 1px solid #334155; }
.card h2 { font-size: 16px; margin-bottom: 15px; color: #94a3b8; display: flex; align-items: center; gap: 8px; }
.card h2 .icon { font-size: 20px; }
.stat-card { text-align: center; }
.stat-value { font-size: 36px; font-weight: bold; color: #3b82f6; }
.stat-label { font-size: 12px; color: #64748b; margin-top: 5px; text-transform: uppercase; }
</style>
</head>
<body>
<div class="header">
<h1>🚀 RPA Vision V3 Dashboard</h1>
<div class="status">
<div class="status-dot" id="statusDot"></div>
<span id="statusText">Connecté</span>
</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('chains')">🔗 Chaînes</div>
<div class="tab" onclick="switchTab('triggers')">⚡ Déclencheurs</div>
<div class="tab" onclick="switchTab('sessions')">📦 Sessions</div>
<div class="tab" onclick="switchTab('performance')">📈 Performance</div>
<div class="tab" onclick="switchTab('metrics')">📊 Métriques</div>
<div class="tab" onclick="switchTab('logs')">📄 Logs</div>
<div class="tab" onclick="switchTab('tests')">🧪 Tests</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>
</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>
</div>
<!-- Tab: Sessions -->
<div id="tab-sessions" class="tab-content">
<div class="card">
<h2><span class="icon">📦</span> Sessions Agent</h2>
<div class="actions-bar" style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px;">
<div style="display:flex;gap:10px;align-items:center;">
<button class="btn btn-primary btn-small" onclick="refreshSessions()">🔄 Actualiser</button>
<label style="display:flex;align-items:center;gap:5px;font-size:13px;color:#94a3b8;cursor:pointer;">
<input type="checkbox" id="hideEmptySessions" checked onchange="refreshSessions()">
Masquer sessions vides
</label>
</div>
<div style="display:flex;gap:10px;align-items:center;">
<span id="emptySessionsInfo" style="font-size:12px;color:#f59e0b;"></span>
<button class="btn btn-warning btn-small" onclick="cleanupEmptySessions()" id="btnCleanup" style="display:none;">
🗑️ Supprimer vides
</button>
</div>
</div>
<div class="session-list" id="sessionList">
<div class="loading"><div class="spinner"></div>Chargement...</div>
</div>
</div>
<!-- Modal pour les screenshots -->
<div id="screenshotModal" class="modal" style="display:none;">
<div class="modal-content">
<span class="modal-close" onclick="closeModal()">&times;</span>
<h3 id="modalTitle">Screenshots de la session</h3>
<div class="screenshot-gallery" id="screenshotGallery"></div>
</div>
</div>
</div>
<!-- Tab: Performance -->
<div id="tab-performance" class="tab-content">
<!-- FAISS Status Panel -->
<div class="card" style="margin-bottom:20px;">
<div style="display:flex;justify-content:space-between;align-items:center;">
<h2><span class="icon">🗄️</span> État de l'Index FAISS</h2>
<button class="btn btn-primary btn-small" onclick="testFaissIndex()" id="btnTestFaiss">
🧪 Tester l'index
</button>
</div>
<div class="grid grid-4" style="margin-top:15px;">
<div style="text-align:center;cursor:help;" title="État de l'index FAISS&#10;✅ Actif = Index chargé et fonctionnel&#10;⚠️ Non trouvé = Aucun index créé&#10;❌ Erreur = Problème de chargement">
<div id="faissStatus" style="font-size:24px;"></div>
<div style="font-size:12px;color:#64748b;margin-top:5px;">Status <span style="color:#3b82f6;"></span></div>
</div>
<div style="text-align:center;cursor:help;" title="Nombre de vecteurs dans l'index&#10;Chaque cible cliquée génère un vecteur&#10;Plus il y a de vecteurs, plus la reconnaissance est précise">
<div id="faissVectors" style="font-size:24px;color:#3b82f6;font-weight:bold;">0</div>
<div style="font-size:12px;color:#64748b;margin-top:5px;">Vecteurs <span style="color:#3b82f6;"></span></div>
</div>
<div style="text-align:center;cursor:help;" title="Taille des vecteurs d'embedding&#10;512 = CLIP ViT-B/32 (standard)&#10;768 = CLIP ViT-L/14 (plus précis)">
<div id="faissDimensions" style="font-size:24px;color:#8b5cf6;font-weight:bold;">-</div>
<div style="font-size:12px;color:#64748b;margin-top:5px;">Dimensions <span style="color:#3b82f6;"></span></div>
</div>
<div style="text-align:center;cursor:help;" title="Type d'index FAISS&#10;Flat = Recherche exhaustive (précis, lent sur gros volumes)&#10;IVF = Index inversé (rapide, recommandé > 10k vecteurs)">
<div id="faissIndexType" style="font-size:24px;color:#f59e0b;font-weight:bold;">-</div>
<div style="font-size:12px;color:#64748b;margin-top:5px;">Type <span style="color:#3b82f6;"></span></div>
</div>
</div>
<div style="margin-top:15px;padding:10px;background:#0f172a;border-radius:8px;font-size:13px;color:#94a3b8;">
<span id="faissDetails">Chargement des détails FAISS...</span>
</div>
<!-- Zone de recommandations -->
<div id="faissRecommendations" style="display:none;margin-top:10px;padding:10px;background:#422006;border:1px solid #f59e0b;border-radius:8px;font-size:13px;color:#fbbf24;">
</div>
<!-- Résultats du test -->
<div id="faissTestResults" style="display:none;margin-top:15px;padding:15px;background:#0f172a;border-radius:8px;">
</div>
</div>
<div class="grid grid-3">
<div class="card stat-card">
<div class="stat-value" id="perfEmbeddings">0</div>
<div class="stat-label">Embeddings</div>
</div>
<div class="card stat-card">
<div class="stat-value" id="perfCacheHits">0%</div>
<div class="stat-label">Cache Hit Rate</div>
</div>
<div class="card stat-card">
<div class="stat-value" id="perfAvgTime">0ms</div>
<div class="stat-label">Temps moyen</div>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<h2><span class="icon">🔍</span> FAISS Performance</h2>
<canvas id="faissChart" height="250"></canvas>
</div>
<div class="card">
<h2><span class="icon">💾</span> Cache Performance</h2>
<canvas id="cacheChart" height="250"></canvas>
</div>
</div>
</div>
<!-- Tab: Chaînes -->
<div id="tab-chains" class="tab-content">
<div class="card">
<h2><span class="icon">🔗</span> Chaînes de Workflows</h2>
<div class="actions-bar">
<button class="btn btn-primary btn-small" onclick="refreshChains()">🔄 Actualiser</button>
<button class="btn btn-success btn-small" onclick="showCreateChainModal()"> Nouvelle Chaîne</button>
</div>
<div class="workflow-grid" id="chainList">
<div class="loading"><div class="spinner"></div>Chargement...</div>
</div>
</div>
</div>
<!-- Tab: Triggers -->
<div id="tab-triggers" class="tab-content">
<div class="card">
<h2><span class="icon"></span> Déclencheurs</h2>
<div class="actions-bar">
<button class="btn btn-primary btn-small" onclick="refreshTriggers()">🔄 Actualiser</button>
<button class="btn btn-success btn-small" onclick="showCreateTriggerModal()"> Nouveau Déclencheur</button>
</div>
<div class="workflow-grid" id="triggerList">
<div class="loading"><div class="spinner"></div>Chargement...</div>
</div>
</div>
</div>
<!-- Tab: Métriques Prometheus -->
<div id="tab-metrics" class="tab-content">
<div class="grid grid-4">
<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">
<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">
<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: 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>
</div>
<style>
.execution-panel { padding: 15px; background: #0f172a; border-radius: 8px; }
.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; }
</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');
event.target.classList.add('active');
if (tabName === 'services') refreshServices();
if (tabName === 'sessions') refreshSessions();
if (tabName === 'workflows') refreshWorkflows();
if (tabName === 'chains') refreshChains();
if (tabName === 'triggers') refreshTriggers();
if (tabName === 'tests') refreshTests();
if (tabName === 'performance') refreshPerformance();
if (tabName === 'metrics') { refreshMetrics(); refreshAutomationStatus(); }
if (tabName === 'logs') refreshLogs();
}
// 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);
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
initOverviewChart();
refreshServices(); // Charger les services en premier
refreshSystemStatus();
refreshWorkflows();
setInterval(refreshSystemStatus, 10000);
setInterval(refreshServices, 5000); // Rafraîchir les services toutes les 5s
});
// 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>