- 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>
1700 lines
83 KiB
HTML
1700 lines
83 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="fr">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>RPA Vision V3 - Dashboard</title>
|
||
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; }
|
||
.header { background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); color: white; padding: 20px 30px; display: flex; justify-content: space-between; align-items: center; }
|
||
.header h1 { font-size: 24px; display: flex; align-items: center; gap: 10px; }
|
||
.header .status { display: flex; align-items: center; gap: 8px; font-size: 14px; }
|
||
.status-dot { width: 10px; height: 10px; border-radius: 50%; background: #22c55e; animation: pulse 2s infinite; }
|
||
.status-dot.offline { background: #ef4444; animation: none; }
|
||
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
||
|
||
.tabs { background: #1e293b; border-bottom: 1px solid #334155; display: flex; padding: 0 20px; }
|
||
.tab { padding: 15px 25px; cursor: pointer; border-bottom: 3px solid transparent; transition: all 0.2s; color: #94a3b8; font-weight: 500; }
|
||
.tab:hover { color: #e2e8f0; background: #334155; }
|
||
.tab.active { border-bottom-color: #3b82f6; color: #3b82f6; }
|
||
|
||
.container { max-width: 1600px; margin: 0 auto; padding: 20px; }
|
||
.tab-content { display: none; }
|
||
.tab-content.active { display: block; }
|
||
|
||
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 20px; }
|
||
.grid-2 { grid-template-columns: 1fr 1fr; }
|
||
.grid-3 { grid-template-columns: 1fr 1fr 1fr; }
|
||
.grid-4 { grid-template-columns: repeat(4, 1fr); }
|
||
|
||
.card { background: #1e293b; border-radius: 12px; padding: 20px; border: 1px solid #334155; }
|
||
.card h2 { font-size: 16px; margin-bottom: 15px; color: #94a3b8; display: flex; align-items: center; gap: 8px; }
|
||
.card h2 .icon { font-size: 20px; }
|
||
|
||
.stat-card { text-align: center; }
|
||
.stat-value { font-size: 36px; font-weight: bold; color: #3b82f6; }
|
||
.stat-label { font-size: 12px; color: #64748b; margin-top: 5px; text-transform: uppercase; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="header">
|
||
<h1>🚀 RPA Vision V3 Dashboard</h1>
|
||
<div 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()">×</span>
|
||
<h3 id="modalTitle">Screenshots de la session</h3>
|
||
<div class="screenshot-gallery" id="screenshotGallery"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tab: Performance -->
|
||
<div id="tab-performance" class="tab-content">
|
||
<!-- FAISS Status Panel -->
|
||
<div class="card" style="margin-bottom:20px;">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||
<h2><span class="icon">🗄️</span> État de l'Index FAISS</h2>
|
||
<button class="btn btn-primary btn-small" onclick="testFaissIndex()" id="btnTestFaiss">
|
||
🧪 Tester l'index
|
||
</button>
|
||
</div>
|
||
<div class="grid grid-4" style="margin-top:15px;">
|
||
<div style="text-align:center;cursor:help;" title="État de l'index FAISS ✅ Actif = Index chargé et fonctionnel ⚠️ Non trouvé = Aucun index créé ❌ Erreur = Problème de chargement">
|
||
<div id="faissStatus" style="font-size:24px;">⏳</div>
|
||
<div style="font-size:12px;color:#64748b;margin-top:5px;">Status <span style="color:#3b82f6;">ⓘ</span></div>
|
||
</div>
|
||
<div style="text-align:center;cursor:help;" title="Nombre de vecteurs dans l'index Chaque cible cliquée génère un vecteur Plus il y a de vecteurs, plus la reconnaissance est précise">
|
||
<div id="faissVectors" style="font-size:24px;color:#3b82f6;font-weight:bold;">0</div>
|
||
<div style="font-size:12px;color:#64748b;margin-top:5px;">Vecteurs <span style="color:#3b82f6;">ⓘ</span></div>
|
||
</div>
|
||
<div style="text-align:center;cursor:help;" title="Taille des vecteurs d'embedding 512 = CLIP ViT-B/32 (standard) 768 = CLIP ViT-L/14 (plus précis)">
|
||
<div id="faissDimensions" style="font-size:24px;color:#8b5cf6;font-weight:bold;">-</div>
|
||
<div style="font-size:12px;color:#64748b;margin-top:5px;">Dimensions <span style="color:#3b82f6;">ⓘ</span></div>
|
||
</div>
|
||
<div style="text-align:center;cursor:help;" title="Type d'index FAISS Flat = Recherche exhaustive (précis, lent sur gros volumes) IVF = Index inversé (rapide, recommandé > 10k vecteurs)">
|
||
<div id="faissIndexType" style="font-size:24px;color:#f59e0b;font-weight:bold;">-</div>
|
||
<div style="font-size:12px;color:#64748b;margin-top:5px;">Type <span style="color:#3b82f6;">ⓘ</span></div>
|
||
</div>
|
||
</div>
|
||
<div style="margin-top:15px;padding:10px;background:#0f172a;border-radius:8px;font-size:13px;color:#94a3b8;">
|
||
<span id="faissDetails">Chargement des détails FAISS...</span>
|
||
</div>
|
||
<!-- Zone de recommandations -->
|
||
<div id="faissRecommendations" style="display:none;margin-top:10px;padding:10px;background:#422006;border:1px solid #f59e0b;border-radius:8px;font-size:13px;color:#fbbf24;">
|
||
</div>
|
||
<!-- Résultats du test -->
|
||
<div id="faissTestResults" style="display:none;margin-top:15px;padding:15px;background:#0f172a;border-radius:8px;">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid grid-3">
|
||
<div class="card stat-card">
|
||
<div class="stat-value" id="perfEmbeddings">0</div>
|
||
<div class="stat-label">Embeddings</div>
|
||
</div>
|
||
<div class="card stat-card">
|
||
<div class="stat-value" id="perfCacheHits">0%</div>
|
||
<div class="stat-label">Cache Hit Rate</div>
|
||
</div>
|
||
<div class="card stat-card">
|
||
<div class="stat-value" id="perfAvgTime">0ms</div>
|
||
<div class="stat-label">Temps moyen</div>
|
||
</div>
|
||
</div>
|
||
<div class="grid grid-2">
|
||
<div class="card">
|
||
<h2><span class="icon">🔍</span> FAISS Performance</h2>
|
||
<canvas id="faissChart" height="250"></canvas>
|
||
</div>
|
||
<div class="card">
|
||
<h2><span class="icon">💾</span> Cache Performance</h2>
|
||
<canvas id="cacheChart" height="250"></canvas>
|
||
</div>
|
||
</div>
|
||
</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>
|