- Frontend v4 accessible sur réseau local (192.168.1.40) - Ports ouverts: 3002 (frontend), 5001 (backend), 5004 (dashboard) - Ollama GPU fonctionnel - Self-healing interactif - Dashboard confiance Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1186 lines
56 KiB
HTML
1186 lines
56 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('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 Brutes</div>
|
||
<div class="tab" onclick="switchTab('processed')">✅ Données Traitées</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: Vue d'ensemble -->
|
||
<div id="tab-overview" class="tab-content active">
|
||
<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">
|
||
<button class="btn btn-primary btn-small" onclick="refreshWorkflows()">🔄 Actualiser</button>
|
||
</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">
|
||
<button class="btn btn-primary btn-small" onclick="refreshSessions()">🔄 Actualiser</button>
|
||
</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: Données Traitées -->
|
||
<div id="tab-processed" class="tab-content">
|
||
<div class="card">
|
||
<h2><span class="icon">✅</span> Données Traitées - Screen States</h2>
|
||
<div class="actions-bar">
|
||
<button class="btn btn-primary btn-small" onclick="refreshProcessedData()">🔄 Actualiser</button>
|
||
</div>
|
||
<div id="processedStats" style="margin-bottom: 20px;">
|
||
<div class="grid grid-4">
|
||
<div class="card stat-card">
|
||
<div class="stat-value" id="statScreenStates">0</div>
|
||
<div class="stat-label">Screen States</div>
|
||
</div>
|
||
<div class="card stat-card">
|
||
<div class="stat-value" id="statProcessedSessions">0</div>
|
||
<div class="stat-label">Sessions Traitées</div>
|
||
</div>
|
||
<div class="card stat-card">
|
||
<div class="stat-value" id="statAvgStates">0</div>
|
||
<div class="stat-label">Moyenne / Session</div>
|
||
</div>
|
||
<div class="card stat-card">
|
||
<div class="stat-value" id="statLastProcessed">-</div>
|
||
<div class="stat-label">Dernière Session</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<h3 style="margin: 20px 0 10px 0; color: #94a3b8;">Sessions Traitées</h3>
|
||
<div class="processed-sessions-list" id="processedSessionsList">
|
||
<div class="loading"><div class="spinner"></div>Chargement...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tab: Performance -->
|
||
<div id="tab-performance" class="tab-content">
|
||
<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; }
|
||
.processed-sessions-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); }
|
||
</style>
|
||
|
||
<script>
|
||
// WebSocket connection
|
||
const socket = io();
|
||
let perfChart, faissChart, cacheChart;
|
||
let executionState = { running: false };
|
||
|
||
// 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 === 'sessions') refreshSessions();
|
||
if (tabName === 'processed') refreshProcessedData();
|
||
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 response = await fetch(`/api/workflows/${workflowId}/execute`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({ mode })
|
||
});
|
||
const data = await response.json();
|
||
if (data.error) alert('Erreur: ' + data.error);
|
||
} catch (e) {
|
||
alert('Erreur: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function stopExecution() {
|
||
if (!executionState.workflow_id) return;
|
||
try {
|
||
await fetch(`/api/workflows/${executionState.workflow_id}/stop`, { method: 'POST' });
|
||
} catch (e) {
|
||
console.error(e);
|
||
}
|
||
}
|
||
|
||
function pauseExecution() {
|
||
alert('Pause non implémentée - utilisez Stop');
|
||
}
|
||
|
||
// Refresh functions
|
||
async function refreshSystemStatus() {
|
||
try {
|
||
const response = await fetch('/api/system/status');
|
||
const data = await response.json();
|
||
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);
|
||
}
|
||
}
|
||
|
||
async function refreshWorkflows() {
|
||
const list = document.getElementById('workflowList');
|
||
list.innerHTML = '<div class="loading"><div class="spinner"></div>Chargement...</div>';
|
||
|
||
try {
|
||
const response = await fetch('/api/workflows');
|
||
const data = await response.json();
|
||
|
||
if (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 response = await fetch(`/api/workflows/${id}`);
|
||
const data = await response.json();
|
||
alert(JSON.stringify(data.workflow, null, 2));
|
||
} catch (e) {
|
||
alert('Erreur: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// Sessions
|
||
async function refreshSessions() {
|
||
const list = document.getElementById('sessionList');
|
||
list.innerHTML = '<div class="loading"><div class="spinner"></div>Chargement...</div>';
|
||
|
||
try {
|
||
const response = await fetch('/api/agent/sessions');
|
||
const data = await response.json();
|
||
|
||
if (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');
|
||
|
||
html += `
|
||
<div class="session-item">
|
||
<div class="session-info">
|
||
<h4>${session.session_id}</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}')">⚙️ 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 response = await fetch(`/api/agent/sessions/${sessionId}`);
|
||
const data = await response.json();
|
||
|
||
if (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 processSession(sessionId) {
|
||
if (!confirm(`Lancer le traitement de ${sessionId}?`)) return;
|
||
|
||
try {
|
||
const response = await fetch(`/api/agent/sessions/${sessionId}/process`, { method: 'POST' });
|
||
const data = await response.json();
|
||
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 response = await fetch('/api/system/performance');
|
||
const data = await response.json();
|
||
|
||
// Update stats
|
||
document.getElementById('perfEmbeddings').textContent = data.faiss?.total_embeddings || 0;
|
||
|
||
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 + '%';
|
||
document.getElementById('perfAvgTime').textContent = (data.faiss?.avg_search_time_ms || 0).toFixed(2) + 'ms';
|
||
|
||
// Update charts
|
||
updatePerformanceCharts(data);
|
||
} catch (e) {
|
||
console.error('Performance error:', e);
|
||
}
|
||
}
|
||
|
||
function updatePerformanceCharts(data) {
|
||
// FAISS Chart
|
||
if (!faissChart) {
|
||
const ctx = document.getElementById('faissChart').getContext('2d');
|
||
faissChart = new Chart(ctx, {
|
||
type: 'line',
|
||
data: {
|
||
labels: [],
|
||
datasets: [{
|
||
label: 'Temps de recherche (ms)',
|
||
data: [],
|
||
borderColor: '#3b82f6',
|
||
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' } }
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Cache Chart
|
||
if (!cacheChart) {
|
||
const ctx = document.getElementById('cacheChart').getContext('2d');
|
||
cacheChart = new Chart(ctx, {
|
||
type: 'doughnut',
|
||
data: {
|
||
labels: ['Hits', 'Misses'],
|
||
datasets: [{
|
||
data: [data.embedding_cache?.hits || 0, data.embedding_cache?.misses || 0],
|
||
backgroundColor: ['#22c55e', '#ef4444']
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
plugins: { legend: { labels: { color: '#94a3b8' } } }
|
||
}
|
||
});
|
||
} else {
|
||
cacheChart.data.datasets[0].data = [data.embedding_cache?.hits || 0, data.embedding_cache?.misses || 0];
|
||
cacheChart.update();
|
||
}
|
||
}
|
||
|
||
// Tests
|
||
async function refreshTests() {
|
||
const list = document.getElementById('testList');
|
||
list.innerHTML = '<div class="loading"><div class="spinner"></div>Chargement...</div>';
|
||
|
||
try {
|
||
const response = await fetch('/api/tests');
|
||
const data = await response.json();
|
||
|
||
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 response = await fetch('/api/tests/run', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({ test_path: testPath })
|
||
});
|
||
|
||
const data = await response.json();
|
||
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 response = await fetch('/api/tests/run-all', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({ type })
|
||
});
|
||
|
||
const data = await response.json();
|
||
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 response = await fetch('/api/chains');
|
||
const data = await response.json();
|
||
|
||
if (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 response = await fetch(`/api/chains/${chainId}/execute`, { method: 'POST' });
|
||
const data = await response.json();
|
||
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 response = await fetch('/api/triggers');
|
||
const data = await response.json();
|
||
|
||
if (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 response = await fetch(`/api/triggers/${triggerId}/toggle`, { method: 'POST' });
|
||
const data = await response.json();
|
||
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 response = await fetch('/api/automation/status');
|
||
const data = await response.json();
|
||
|
||
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 + '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);
|
||
}
|
||
}
|
||
|
||
async function toggleAutomation() {
|
||
try {
|
||
const statusResponse = await fetch('/api/automation/status');
|
||
const statusData = await statusResponse.json();
|
||
|
||
const endpoint = statusData.running ? '/api/automation/stop' : '/api/automation/start';
|
||
const response = await fetch(endpoint, { method: 'POST' });
|
||
const data = await response.json();
|
||
|
||
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');
|
||
const text = await response.text();
|
||
output.textContent = text;
|
||
} catch (e) {
|
||
output.textContent = 'Erreur: ' + e.message;
|
||
}
|
||
}
|
||
|
||
// Processed Data
|
||
async function refreshProcessedData() {
|
||
const list = document.getElementById('processedSessionsList');
|
||
list.innerHTML = '<div class="loading"><div class="spinner"></div>Chargement...</div>';
|
||
|
||
try {
|
||
const response = await fetch('/api/screen_states');
|
||
const data = await response.json();
|
||
|
||
// Mettre à jour les stats
|
||
document.getElementById('statScreenStates').textContent = data.total || 0;
|
||
document.getElementById('statProcessedSessions').textContent = data.sessions_count || 0;
|
||
|
||
const avgStates = data.sessions_count > 0 ?
|
||
Math.round(data.total / data.sessions_count) : 0;
|
||
document.getElementById('statAvgStates').textContent = avgStates;
|
||
|
||
// Dernière session traitée
|
||
if (data.sessions_grouped && data.sessions_grouped.length > 0) {
|
||
const lastSession = data.sessions_grouped[0];
|
||
const lastDate = new Date(lastSession.first_timestamp).toLocaleDateString('fr-FR');
|
||
document.getElementById('statLastProcessed').textContent = lastDate;
|
||
} else {
|
||
document.getElementById('statLastProcessed').textContent = '-';
|
||
}
|
||
|
||
if (data.sessions_grouped.length === 0) {
|
||
list.innerHTML = '<p style="text-align:center;color:#64748b;padding:40px;">Aucune session traitée</p>';
|
||
return;
|
||
}
|
||
|
||
let html = '';
|
||
data.sessions_grouped.forEach(session => {
|
||
const startDate = new Date(session.first_timestamp).toLocaleString('fr-FR');
|
||
const endDate = new Date(session.last_timestamp).toLocaleString('fr-FR');
|
||
const tags = session.tags.join(', ') || 'Aucun tag';
|
||
|
||
html += `
|
||
<div class="session-item" style="cursor: pointer;" onclick="viewProcessedSession('${session.session_id}')">
|
||
<div class="session-info">
|
||
<h4>${session.session_id}</h4>
|
||
<div class="meta">
|
||
👤 ${session.user_id} • 🏷️ ${tags}<br>
|
||
📊 ${session.count} screen states • 📅 ${startDate}
|
||
</div>
|
||
</div>
|
||
<div class="session-actions">
|
||
<button class="btn btn-primary btn-small" onclick="event.stopPropagation(); viewProcessedSession('${session.session_id}')">
|
||
👁️ Voir Détails
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
list.innerHTML = html;
|
||
} catch (e) {
|
||
list.innerHTML = '<p style="color:#ef4444;">Erreur: ' + e.message + '</p>';
|
||
}
|
||
}
|
||
|
||
async function viewProcessedSession(sessionId) {
|
||
try {
|
||
const response = await fetch(`/api/screen_states/${sessionId}`);
|
||
const data = await response.json();
|
||
|
||
let details = `Session: ${sessionId}\nTotal: ${data.total} screen states\n\n`;
|
||
details += 'Premiers screen states:\n';
|
||
data.screen_states.slice(0, 5).forEach((state, i) => {
|
||
details += `\n${i+1}. ${state.screen_state_id}\n`;
|
||
details += ` Timestamp: ${state.timestamp}\n`;
|
||
details += ` App: ${state.window?.app_name || 'unknown'}\n`;
|
||
details += ` Title: ${state.window?.window_title || 'unknown'}\n`;
|
||
});
|
||
|
||
alert(details);
|
||
} catch (e) {
|
||
alert('Erreur: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// Logs
|
||
async function refreshLogs() {
|
||
const output = document.getElementById('logsOutput');
|
||
output.textContent = '⏳ Chargement des logs...\n';
|
||
|
||
try {
|
||
const response = await fetch('/api/logs');
|
||
const data = await response.json();
|
||
|
||
if (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' } }
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Initialize
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
initOverviewChart();
|
||
refreshSystemStatus();
|
||
refreshWorkflows();
|
||
setInterval(refreshSystemStatus, 10000);
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|