Files
rpa_vision_v3/web_dashboard/templates/index.html
Dom 2922b6dda8 feat(dashboard): Ajouter sections Sauvegardes, Système, Corrections et Apprentissage
- Section Sauvegardes: export par catégorie (workflows, corrections, modèles, config) et backup complet
- Section Système: infos version, Python, OS, points de restauration, upload packages mise à jour
- Section Corrections: gestion des Correction Packs (liste, création, export, stats)
- Section Apprentissage: stats corpus FAISS, sessions traitées, graphiques évolution, top corrections

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 19:23:15 +01:00

2542 lines
125 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 class="tab" onclick="switchTab('backups')">💾 Sauvegardes</div>
<div class="tab" onclick="switchTab('system')">⚙️ Système</div>
<div class="tab" onclick="switchTab('corrections')">🔧 Corrections</div>
<div class="tab" onclick="switchTab('learning')">🧠 Apprentissage</div>
</div>
<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>
<!-- 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: Système -->
<div id="tab-system" class="tab-content">
<div class="card" style="margin-bottom:20px;background:linear-gradient(135deg, #1e293b 0%, #0f172a 100%);">
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:15px;">
<div>
<h2 style="margin-bottom:5px;color:#e2e8f0;"><span class="icon">⚙️</span> Système & Version</h2>
<p style="color:#64748b;font-size:13px;">Informations système, mises à jour et points de restauration</p>
</div>
<button class="btn btn-primary" onclick="refreshSystemInfo()">🔄 Actualiser</button>
</div>
</div>
<div class="grid grid-3">
<div class="card stat-card">
<div class="stat-value" id="sysVersion">-</div>
<div class="stat-label">Version</div>
</div>
<div class="card stat-card">
<div class="stat-value" id="sysCommit">-</div>
<div class="stat-label">Commit</div>
</div>
<div class="card stat-card">
<div id="sysUpdateStatus" style="font-size:24px;"></div>
<div class="stat-label">Mise à jour</div>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<h2><span class="icon">🖥️</span> Informations système</h2>
<div class="system-info" id="systemInfoDetails">
<div class="loading"><div class="spinner"></div>Chargement...</div>
</div>
</div>
<div class="card">
<h2><span class="icon">⏮️</span> Points de restauration</h2>
<div class="actions-bar">
<button class="btn btn-success btn-small" onclick="createVersionBackup()"> Créer un point</button>
</div>
<div class="backup-list" id="versionBackupsList">
<div class="loading"><div class="spinner"></div>Chargement...</div>
</div>
</div>
</div>
<div class="card" style="margin-top:20px;">
<h2><span class="icon">📤</span> Mise à jour manuelle</h2>
<div style="display:flex;gap:20px;align-items:center;flex-wrap:wrap;">
<div style="flex:1;min-width:300px;">
<p style="color:#94a3b8;margin-bottom:15px;">Uploadez un package de mise à jour (.zip) contenant un manifest valide</p>
<input type="file" id="updateFileInput" accept=".zip" style="display:none;" onchange="uploadUpdatePackage()">
<button class="btn btn-warning" onclick="document.getElementById('updateFileInput').click()">
📤 Uploader un package
</button>
</div>
<div id="updatePackageInfo" style="flex:1;min-width:300px;padding:15px;background:#0f172a;border-radius:8px;display:none;">
</div>
</div>
</div>
</div>
<!-- Tab: Correction Packs -->
<div id="tab-corrections" class="tab-content">
<div class="card" style="margin-bottom:20px;background:linear-gradient(135deg, #1e293b 0%, #0f172a 100%);">
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:15px;">
<div>
<h2 style="margin-bottom:5px;color:#e2e8f0;"><span class="icon">🔧</span> Correction Packs</h2>
<p style="color:#64748b;font-size:13px;">Capitalisez les corrections utilisateur pour améliorer l'auto-healing cross-workflow</p>
</div>
<div style="display:flex;gap:10px;">
<button class="btn btn-success" onclick="showCreatePackModal()"> Nouveau Pack</button>
<button class="btn btn-primary" onclick="refreshCorrectionPacks()">🔄 Actualiser</button>
</div>
</div>
</div>
<div class="grid grid-4">
<div class="card stat-card">
<div class="stat-value" id="statPacks">-</div>
<div class="stat-label">Packs</div>
</div>
<div class="card stat-card">
<div class="stat-value" id="statTotalCorrections">-</div>
<div class="stat-label">Corrections</div>
</div>
<div class="card stat-card">
<div class="stat-value" id="statApplications">-</div>
<div class="stat-label">Applications</div>
</div>
<div class="card stat-card">
<div class="stat-value" id="statSuccessRate">-</div>
<div class="stat-label">Taux succès</div>
</div>
</div>
<div class="card">
<h2><span class="icon">📦</span> Packs disponibles</h2>
<div class="correction-packs-list" id="correctionPacksList">
<div class="loading"><div class="spinner"></div>Chargement...</div>
</div>
</div>
<!-- Modal création pack -->
<div id="createPackModal" class="modal" style="display:none;">
<div class="modal-content" style="max-width:500px;">
<span class="modal-close" onclick="closeCreatePackModal()">&times;</span>
<h3> Créer un nouveau pack</h3>
<div style="margin-top:20px;">
<label style="display:block;color:#94a3b8;margin-bottom:5px;">Nom du pack</label>
<input type="text" id="packName" class="select-input" placeholder="Ex: Corrections SAP">
<label style="display:block;color:#94a3b8;margin:15px 0 5px;">Description</label>
<textarea id="packDescription" class="select-input" rows="3" placeholder="Description optionnelle..."></textarea>
<label style="display:block;color:#94a3b8;margin:15px 0 5px;">Catégorie</label>
<input type="text" id="packCategory" class="select-input" placeholder="Ex: erp, web, desktop">
<div style="display:flex;gap:10px;margin-top:20px;">
<button class="btn btn-success" onclick="createCorrectionPack()">✅ Créer</button>
<button class="btn btn-secondary" onclick="closeCreatePackModal()">Annuler</button>
</div>
</div>
</div>
</div>
</div>
<!-- Tab: Apprentissage -->
<div id="tab-learning" class="tab-content">
<div class="card" style="margin-bottom:20px;background:linear-gradient(135deg, #1e293b 0%, #0f172a 100%);">
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:15px;">
<div>
<h2 style="margin-bottom:5px;color:#e2e8f0;"><span class="icon">🧠</span> Apprentissage & Corpus</h2>
<p style="color:#64748b;font-size:13px;">Statistiques d'apprentissage et évolution du corpus de données</p>
</div>
<button class="btn btn-primary" onclick="refreshLearningStats()">🔄 Actualiser</button>
</div>
</div>
<div class="grid grid-4">
<div class="card stat-card">
<div class="stat-value" id="statCorpusSize">-</div>
<div class="stat-label">Taille corpus</div>
</div>
<div class="card stat-card">
<div class="stat-value" id="statTrainedSessions">-</div>
<div class="stat-label">Sessions traitées</div>
</div>
<div class="card stat-card">
<div class="stat-value" id="statHealingSuccess">-</div>
<div class="stat-label">Self-healing</div>
</div>
<div class="card stat-card">
<div class="stat-value" id="statLearningRate">-</div>
<div class="stat-label">Taux apprentissage</div>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<h2><span class="icon">📈</span> Évolution du corpus</h2>
<canvas id="corpusChart" height="250"></canvas>
</div>
<div class="card">
<h2><span class="icon">🎯</span> Performance self-healing</h2>
<canvas id="healingChart" height="250"></canvas>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<h2><span class="icon">📊</span> Répartition par type d'action</h2>
<div id="actionTypeStats" style="padding:15px;">
<div class="loading"><div class="spinner"></div>Chargement...</div>
</div>
</div>
<div class="card">
<h2><span class="icon">🏆</span> Top corrections appliquées</h2>
<div id="topCorrections" style="padding:15px;">
<div class="loading"><div class="spinner"></div>Chargement...</div>
</div>
</div>
</div>
</div>
</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; }
/* 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');
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();
if (tabName === 'backups') refreshBackupStats();
if (tabName === 'system') refreshSystemInfo();
if (tabName === 'corrections') refreshCorrectionPacks();
if (tabName === 'learning') refreshLearningStats();
}
// 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');
document.getElementById('statWorkflowsBackup').textContent = data.categories?.workflows?.count || 0;
document.getElementById('statCorrectionsBackup').textContent = data.categories?.correction_packs?.count || 0;
document.getElementById('statModelsBackup').textContent = data.categories?.trained_models?.count || 0;
document.getElementById('statSessionsBackup').textContent = data.categories?.sessions?.count || 0;
} 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
const version = await fetchJSON('/api/version');
document.getElementById('sysVersion').textContent = version.version || '-';
document.getElementById('sysCommit').textContent = (version.git_commit || '-').substring(0, 7);
// System info
const sysInfo = await fetchJSON('/api/version/system-info');
const infoDiv = document.getElementById('systemInfoDetails');
infoDiv.innerHTML = `
<div class="system-info-row"><span>Python</span><span>${sysInfo.python_version || '-'}</span></div>
<div class="system-info-row"><span>OS</span><span>${sysInfo.os || '-'}</span></div>
<div class="system-info-row"><span>Architecture</span><span>${sysInfo.architecture || '-'}</span></div>
<div class="system-info-row"><span>CPU Cores</span><span>${sysInfo.cpu_count || '-'}</span></div>
<div class="system-info-row"><span>Mémoire</span><span>${sysInfo.memory_total ? Math.round(sysInfo.memory_total / 1024 / 1024 / 1024) + ' GB' : '-'}</span></div>
<div class="system-info-row"><span>Démarré le</span><span>${version.build_date ? new Date(version.build_date).toLocaleString('fr-FR') : '-'}</span></div>
`;
// 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')}</span>
</div>
<button class="btn btn-warning btn-small" onclick="restoreVersion('${b.id || b.path}')">⏮️ 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) {
if (!confirm('Restaurer ce point ? Les données actuelles seront sauvegardées automatiquement.')) return;
showNotification('⏮️ Restauration en cours...', 'warning');
try {
await fetchJSON(`/api/version/rollback/${backupId}`, { method: 'POST' });
showNotification('✅ Restauration effectuée. Redémarrage recommandé.', 'success');
} catch (e) {
showNotification(`❌ Erreur: ${e.message}`, 'error');
}
}
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 5000)
let data;
try {
data = await fetchJSON('http://localhost:5000/api/correction-packs/stats');
} catch (e) {
// Fallback sur les stats de backup
const backup = await fetchJSON('/api/backup/stats');
data = {
total_packs: backup.categories?.correction_packs?.count || 0,
total_corrections: 0,
total_applications: 0,
overall_success_rate: 0
};
}
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:5000/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:5000/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:5000/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:5000/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
const faiss = perf.faiss || {};
document.getElementById('statCorpusSize').textContent = faiss.vectors || 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:5000/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');
// Données simulées basées sur les types d'actions disponibles
const actionTypes = [
{ name: 'click', count: 45, color: '#3b82f6' },
{ name: 'type_text', count: 28, color: '#8b5cf6' },
{ name: 'wait_for_anchor', count: 15, color: '#22c55e' },
{ name: 'scroll', count: 8, color: '#f59e0b' },
{ name: 'hotkey', count: 4, color: '#ef4444' }
];
const total = actionTypes.reduce((s, a) => s + a.count, 0);
div.innerHTML = actionTypes.map(a => {
const pct = total > 0 ? Math.round(a.count / total * 100) : 0;
return `
<div class="stat-bar">
<span class="stat-bar-label">${a.name}</span>
<span class="stat-bar-value">${a.count} (${pct}%)</span>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width:${pct}%;background:${a.color};"></div>
</div>
`;
}).join('');
}
async function loadTopCorrections() {
const div = document.getElementById('topCorrections');
try {
let corrections = [];
try {
const data = await fetchJSON('http://localhost:5000/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' } }
}
}
});
}
}
// 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
// Charger les stats des nouvelles sections (en arrière-plan)
setTimeout(() => {
refreshBackupStats();
refreshSystemInfo();
}, 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>