Files
rpa_vision_v3/dashboard_index.html
Dom a27b74cf22 v1.0 - Version stable: multi-PC, détection UI-DETR-1, 3 modes exécution
- 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>
2026-01-29 11:23:51 +01:00

1186 lines
56 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RPA Vision V3 - Dashboard</title>
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; }
.header { background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); color: white; padding: 20px 30px; display: flex; justify-content: space-between; align-items: center; }
.header h1 { font-size: 24px; display: flex; align-items: center; gap: 10px; }
.header .status { display: flex; align-items: center; gap: 8px; font-size: 14px; }
.status-dot { width: 10px; height: 10px; border-radius: 50%; background: #22c55e; animation: pulse 2s infinite; }
.status-dot.offline { background: #ef4444; animation: none; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
.tabs { background: #1e293b; border-bottom: 1px solid #334155; display: flex; padding: 0 20px; }
.tab { padding: 15px 25px; cursor: pointer; border-bottom: 3px solid transparent; transition: all 0.2s; color: #94a3b8; font-weight: 500; }
.tab:hover { color: #e2e8f0; background: #334155; }
.tab.active { border-bottom-color: #3b82f6; color: #3b82f6; }
.container { max-width: 1600px; margin: 0 auto; padding: 20px; }
.tab-content { display: none; }
.tab-content.active { display: block; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 20px; }
.grid-2 { grid-template-columns: 1fr 1fr; }
.grid-3 { grid-template-columns: 1fr 1fr 1fr; }
.grid-4 { grid-template-columns: repeat(4, 1fr); }
.card { background: #1e293b; border-radius: 12px; padding: 20px; border: 1px solid #334155; }
.card h2 { font-size: 16px; margin-bottom: 15px; color: #94a3b8; display: flex; align-items: center; gap: 8px; }
.card h2 .icon { font-size: 20px; }
.stat-card { text-align: center; }
.stat-value { font-size: 36px; font-weight: bold; color: #3b82f6; }
.stat-label { font-size: 12px; color: #64748b; margin-top: 5px; text-transform: uppercase; }
</style>
</head>
<body>
<div class="header">
<h1>🚀 RPA Vision V3 Dashboard</h1>
<div class="status">
<div class="status-dot" id="statusDot"></div>
<span id="statusText">Connecté</span>
</div>
</div>
<div class="tabs">
<div class="tab active" onclick="switchTab('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()">&times;</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>