refactor: factorisation input_handler partagé + page cartographie processus
Some checks failed
security-audit / Bandit (scan statique) (push) Successful in 12s
security-audit / pip-audit (CVE dépendances) (push) Successful in 11s
security-audit / Scan secrets (grep) (push) Successful in 8s
tests / Lint (ruff + black) (push) Successful in 14s
tests / Tests unitaires (sans GPU) (push) Failing after 14s
tests / Tests sécurité (critique) (push) Has been skipped

core/execution/input_handler.py (NOUVEAU) :
- safe_type_text() : setxkbmap fr + xdotool, partagé entre les 2 executors
- check_screen_for_patterns() : détection dialogues UI via OCR
- handle_detected_pattern() : clic bouton par OCR (mot exact, le plus bas)
- post_execution_cleanup() : vérification post-workflow

VWB executor : suppression du code dupliqué, alias vers input_handler
Core executor : pyautogui.write() remplacé par safe_type_text()

Page dashboard "Cartographie des processus" :
- GET /process-mining : vue analyse des flux de travail
- POST /api/process-mining/discover : génère BPMN + indicateurs
- 4 cartes indicateurs, diagramme, points d'attention, variantes
- Dark theme, français, zéro jargon technique
- Onglet ajouté dans la navigation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-04-20 17:08:37 +02:00
parent 447fbb2c6e
commit 6c7f88c05d
6 changed files with 727 additions and 245 deletions

View File

@@ -0,0 +1,471 @@
<!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 - Cartographie des processus</title>
<style>
/* === Reset & base — identique au dashboard === */
* { 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 === */
.header { background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); color: white; padding: 20px 30px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px; }
.header h1 { font-size: 24px; display: flex; align-items: center; gap: 10px; }
.header-subtitle { color: rgba(255,255,255,0.75); font-size: 13px; margin-top: 4px; }
.header-nav { display: flex; align-items: center; gap: 8px; }
.header-nav a {
color: rgba(255,255,255,0.8); text-decoration: none; font-size: 13px;
padding: 6px 14px; border-radius: 6px; transition: all 0.2s;
background: rgba(255,255,255,0.1);
}
.header-nav a:hover { background: rgba(255,255,255,0.2); }
.header-nav a.active { background: rgba(255,255,255,0.25); color: #fff; font-weight: 600; }
/* === Layout === */
.container { max-width: 1600px; margin: 0 auto; padding: 20px; }
/* === Cards === */
.card { background: #1e293b; border-radius: 12px; padding: 20px; border: 1px solid #334155; margin-bottom: 20px; }
.card h2 { font-size: 16px; margin-bottom: 15px; color: #94a3b8; display: flex; align-items: center; gap: 8px; }
/* === Grille indicateurs === */
.grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 20px; }
@media (max-width: 900px) { .grid-4 { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 500px) { .grid-4 { grid-template-columns: 1fr; } }
.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; }
/* === Boutons === */
.btn { padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s; display: inline-flex; align-items: center; gap: 6px; }
.btn-primary { background: #3b82f6; color: white; }
.btn-primary:hover { background: #2563eb; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
/* === Sélecteur === */
.selector-bar {
display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-end;
margin-bottom: 20px; padding: 20px; background: #1e293b;
border-radius: 12px; border: 1px solid #334155;
}
.selector-group { display: flex; flex-direction: column; gap: 4px; }
.selector-group label { font-size: 11px; color: #64748b; text-transform: uppercase; font-weight: 600; }
.selector-input {
padding: 10px 14px; background: #0f172a; border: 1px solid #334155;
border-radius: 8px; color: #e2e8f0; font-size: 14px; min-width: 250px;
}
.selector-input:focus { border-color: #3b82f6; outline: none; }
/* === Image cartographie === */
.process-image-container {
background: #ffffff; border-radius: 12px; padding: 20px;
text-align: center; margin-bottom: 20px; overflow-x: auto;
}
.process-image-container img {
max-width: 100%; height: auto; border-radius: 8px;
}
.image-legend {
color: #64748b; font-size: 12px; margin-top: 12px;
font-style: italic;
}
/* === Points d'attention === */
.bottleneck-list { list-style: none; padding: 0; }
.bottleneck-item {
display: flex; align-items: center; gap: 12px;
padding: 12px 16px; background: #0f172a; border-radius: 8px;
margin-bottom: 8px; border-left: 3px solid #f59e0b;
}
.bottleneck-icon { font-size: 20px; flex-shrink: 0; }
.bottleneck-text { font-size: 14px; color: #e2e8f0; }
.bottleneck-duration { color: #f59e0b; font-weight: 600; }
/* === Variantes === */
.variant-item {
display: flex; justify-content: space-between; align-items: center;
padding: 10px 14px; background: #0f172a; border-radius: 8px;
margin-bottom: 6px; font-size: 13px;
}
.variant-path { color: #94a3b8; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 80%; }
.variant-count { color: #3b82f6; font-weight: 600; white-space: nowrap; }
/* === Distribution apps === */
.app-dist-bar {
display: flex; align-items: center; gap: 10px;
padding: 8px 0; font-size: 13px;
}
.app-dist-name { min-width: 140px; color: #94a3b8; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.app-dist-track { flex: 1; height: 20px; background: #0f172a; border-radius: 4px; overflow: hidden; }
.app-dist-fill { height: 100%; background: linear-gradient(90deg, #3b82f6, #8b5cf6); border-radius: 4px; transition: width 0.5s ease; }
.app-dist-val { min-width: 40px; text-align: right; color: #e2e8f0; font-weight: 500; }
/* === Loading === */
.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 15px; }
@keyframes spin { to { transform: rotate(360deg); } }
/* === Placeholder === */
.placeholder-zone {
text-align: center; padding: 60px 30px; color: #475569;
background: #1e293b; border-radius: 12px; border: 2px dashed #334155;
}
.placeholder-zone .icon { font-size: 48px; margin-bottom: 15px; display: block; }
.placeholder-zone p { font-size: 15px; line-height: 1.6; }
/* === Erreur === */
.error-banner {
background: #7f1d1d; border: 1px solid #ef4444; border-radius: 8px;
padding: 12px 20px; color: #fca5a5; font-size: 13px; margin-bottom: 15px;
display: none; align-items: center; gap: 10px;
}
.error-banner.visible { display: flex; }
/* === Grille détails === */
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
@media (max-width: 900px) { .grid-2 { grid-template-columns: 1fr; } }
/* === Toggle views === */
.view-toggle { display: flex; gap: 8px; margin-bottom: 15px; }
.view-toggle .toggle-btn {
padding: 8px 16px; background: #0f172a; border: 1px solid #334155;
border-radius: 6px; color: #94a3b8; cursor: pointer; font-size: 13px;
transition: all 0.2s;
}
.view-toggle .toggle-btn:hover { background: #334155; }
.view-toggle .toggle-btn.active { background: #3b82f6; color: white; border-color: #3b82f6; }
</style>
</head>
<body>
<!-- Header -->
<div class="header">
<div>
<h1>&#x1F5FA;&#xFE0F; Cartographie des processus</h1>
<div class="header-subtitle">Analyse automatique des flux de travail observes par Lea</div>
</div>
<nav class="header-nav">
<a href="/">&#x1F39B;&#xFE0F; Dashboard</a>
<a href="/audit">&#x2696;&#xFE0F; Audit</a>
<a href="/process-mining" class="active">&#x1F5FA;&#xFE0F; Cartographie</a>
</nav>
</div>
<div class="container">
<!-- Banniere erreur -->
<div class="error-banner" id="errorBanner">
&#x26A0;&#xFE0F; <span id="errorText"></span>
</div>
<!-- Selecteur machine + bouton -->
<div class="selector-bar">
<div class="selector-group">
<label>Machine</label>
<select class="selector-input" id="machineSelect">
<option value="">Toutes les machines</option>
</select>
</div>
<button class="btn btn-primary" id="analyzeBtn" onclick="launchAnalysis()">
&#x1F50D; Analyser
</button>
<span id="analyzeStatus" style="color:#64748b;font-size:13px;align-self:center;"></span>
</div>
<!-- Placeholder (avant analyse) -->
<div id="placeholder" class="placeholder-zone">
<span class="icon">&#x1F50D;</span>
<p>
Selectionnez une machine (ou gardez "Toutes") puis cliquez sur <strong>Analyser</strong>
pour generer la cartographie des processus observes par Lea.
</p>
</div>
<!-- Loading -->
<div id="loadingZone" style="display:none;" class="loading">
<div class="spinner"></div>
<p>Analyse en cours... Cela peut prendre quelques secondes.</p>
</div>
<!-- Resultats (masques au depart) -->
<div id="resultsZone" style="display:none;">
<!-- Indicateurs -->
<div class="grid-4" id="kpiGrid">
<div class="card stat-card">
<div class="stat-value" id="kpiSessions">-</div>
<div class="stat-label">Sessions analysees</div>
</div>
<div class="card stat-card">
<div class="stat-value" id="kpiActivities">-</div>
<div class="stat-label">Activites detectees</div>
</div>
<div class="card stat-card">
<div class="stat-value" id="kpiDuration">-</div>
<div class="stat-label">Duree moyenne</div>
</div>
<div class="card stat-card">
<div class="stat-value" id="kpiVariants">-</div>
<div class="stat-label">Variantes de parcours</div>
</div>
</div>
<!-- Cartographie visuelle -->
<div class="card">
<h2>&#x1F5FA;&#xFE0F; Cartographie visuelle</h2>
<div class="view-toggle">
<button class="toggle-btn active" onclick="switchView('bpmn', this)">Vue structurelle</button>
<button class="toggle-btn" onclick="switchView('dfg', this)">Vue flux</button>
</div>
<div id="bpmnView" class="process-image-container">
<img id="bpmnImage" src="" alt="Cartographie des processus" />
<p class="image-legend">Chaque rectangle represente une etape du processus observe. Les fleches indiquent l'ordre des actions.</p>
</div>
<div id="dfgView" class="process-image-container" style="display:none;">
<img id="dfgImage" src="" alt="Graphe de flux" />
<p class="image-legend">Chaque noeud represente une action. Les chiffres sur les fleches indiquent la frequence de passage.</p>
</div>
</div>
<!-- Grille bas : goulots + variantes -->
<div class="grid-2">
<!-- Points d'attention (goulots) -->
<div class="card">
<h2>&#x23F1;&#xFE0F; Points d'attention</h2>
<p style="color:#64748b;font-size:12px;margin-bottom:12px;">Les etapes qui prennent le plus de temps en moyenne</p>
<ul class="bottleneck-list" id="bottleneckList">
<li class="bottleneck-item" style="color:#475569;">Aucune donnee</li>
</ul>
</div>
<!-- Distribution par application -->
<div class="card">
<h2>&#x1F4CA; Repartition par application</h2>
<p style="color:#64748b;font-size:12px;margin-bottom:12px;">Nombre d'actions par logiciel utilise</p>
<div id="appDistribution">
<p style="color:#475569;">Aucune donnee</p>
</div>
</div>
</div>
<!-- Top variantes -->
<div class="card">
<h2>&#x1F500; Principaux chemins observes</h2>
<p style="color:#64748b;font-size:12px;margin-bottom:12px;">Les enchainements d'actions les plus frequents</p>
<div id="variantsList">
<p style="color:#475569;">Aucune donnee</p>
</div>
</div>
</div><!-- /resultsZone -->
</div><!-- /container -->
<script>
// ========================================================================
// Chargement des machines disponibles
// ========================================================================
async function loadMachines() {
try {
const resp = await fetch('/api/process-mining/machines');
if (!resp.ok) return;
const data = await resp.json();
const select = document.getElementById('machineSelect');
(data.machines || []).forEach(m => {
const opt = document.createElement('option');
opt.value = m.machine_id;
opt.textContent = m.machine_id + ' (' + m.sessions_count + ' sessions)';
select.appendChild(opt);
});
} catch (e) {
console.error('Erreur chargement machines:', e);
}
}
// ========================================================================
// Lancement de l'analyse
// ========================================================================
async function launchAnalysis() {
const machineId = document.getElementById('machineSelect').value;
const btn = document.getElementById('analyzeBtn');
const status = document.getElementById('analyzeStatus');
// UI : loading
btn.disabled = true;
status.textContent = 'Analyse en cours...';
document.getElementById('placeholder').style.display = 'none';
document.getElementById('resultsZone').style.display = 'none';
document.getElementById('loadingZone').style.display = 'block';
hideError();
try {
const resp = await fetch('/api/process-mining/discover', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ machine_id: machineId || '' }),
});
const data = await resp.json();
if (!resp.ok || data.error) {
showError(data.error || 'Erreur inconnue', data.detail || '');
document.getElementById('loadingZone').style.display = 'none';
document.getElementById('placeholder').style.display = 'block';
btn.disabled = false;
status.textContent = '';
return;
}
// Afficher les resultats
renderResults(data);
document.getElementById('loadingZone').style.display = 'none';
document.getElementById('resultsZone').style.display = 'block';
status.textContent = 'Analyse terminee';
} catch (e) {
showError('Erreur de communication avec le serveur', e.message);
document.getElementById('loadingZone').style.display = 'none';
document.getElementById('placeholder').style.display = 'block';
} finally {
btn.disabled = false;
}
}
// ========================================================================
// Affichage des resultats
// ========================================================================
function renderResults(data) {
const kpis = data.kpis || {};
// -- Indicateurs --
document.getElementById('kpiSessions').textContent = data.sessions_loaded || 0;
document.getElementById('kpiActivities').textContent = kpis.unique_activities || 0;
document.getElementById('kpiVariants').textContent = kpis.variants_count || 0;
// Duree moyenne formatee
const avgDur = kpis.avg_case_duration_seconds || 0;
document.getElementById('kpiDuration').textContent = formatDuration(avgDur);
// -- Image BPMN --
const bpmnImg = document.getElementById('bpmnImage');
if (data.bpmn_image_url) {
bpmnImg.src = data.bpmn_image_url + '?t=' + Date.now();
bpmnImg.style.display = 'block';
} else {
bpmnImg.style.display = 'none';
}
// -- Image DFG --
const dfgImg = document.getElementById('dfgImage');
if (data.dfg_image_url) {
dfgImg.src = data.dfg_image_url + '?t=' + Date.now();
dfgImg.style.display = 'block';
} else {
dfgImg.style.display = 'none';
}
// -- Goulots --
const bottlenecks = kpis.bottlenecks || [];
const bnList = document.getElementById('bottleneckList');
if (bottlenecks.length === 0) {
bnList.innerHTML = '<li class="bottleneck-item" style="color:#475569;">Aucun goulot detecte</li>';
} else {
bnList.innerHTML = bottlenecks.map((b, i) => {
const icons = ['\u23F1\uFE0F', '\u26A0\uFE0F', '\u{1F4CB}'];
return '<li class="bottleneck-item">' +
'<span class="bottleneck-icon">' + (icons[i] || '\u{1F4CB}') + '</span>' +
'<span class="bottleneck-text">L\'etape <strong>' + escapeHtml(b.activity) +
'</strong> prend en moyenne <span class="bottleneck-duration">' +
formatDuration(b.avg_duration_seconds) + '</span></span>' +
'</li>';
}).join('');
}
// -- Distribution par application --
const appDist = kpis.app_distribution || {};
const appDiv = document.getElementById('appDistribution');
const appEntries = Object.entries(appDist).sort((a, b) => b[1] - a[1]);
if (appEntries.length === 0) {
appDiv.innerHTML = '<p style="color:#475569;">Aucune donnee</p>';
} else {
const maxVal = appEntries[0][1];
appDiv.innerHTML = appEntries.slice(0, 8).map(([name, count]) => {
const pct = Math.round((count / maxVal) * 100);
return '<div class="app-dist-bar">' +
'<span class="app-dist-name" title="' + escapeHtml(name) + '">' + escapeHtml(name || 'inconnu') + '</span>' +
'<div class="app-dist-track"><div class="app-dist-fill" style="width:' + pct + '%"></div></div>' +
'<span class="app-dist-val">' + count + '</span>' +
'</div>';
}).join('');
}
// -- Top variantes --
const variants = kpis.variants_top5 || [];
const varDiv = document.getElementById('variantsList');
if (variants.length === 0) {
varDiv.innerHTML = '<p style="color:#475569;">Aucune variante detectee</p>';
} else {
varDiv.innerHTML = variants.map((v, i) => {
return '<div class="variant-item">' +
'<span class="variant-path" title="' + escapeHtml(v.variant) + '">' +
'#' + (i + 1) + ' &mdash; ' + escapeHtml(v.variant) +
'</span>' +
'<span class="variant-count">' + v.count + ' fois</span>' +
'</div>';
}).join('');
}
}
// ========================================================================
// Toggle vue BPMN / DFG
// ========================================================================
function switchView(view, btn) {
document.querySelectorAll('.view-toggle .toggle-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.getElementById('bpmnView').style.display = (view === 'bpmn') ? 'block' : 'none';
document.getElementById('dfgView').style.display = (view === 'dfg') ? 'block' : 'none';
}
// ========================================================================
// Helpers
// ========================================================================
function formatDuration(seconds) {
if (!seconds || seconds <= 0) return '0s';
if (seconds < 60) return Math.round(seconds) + 's';
if (seconds < 3600) {
const m = Math.floor(seconds / 60);
const s = Math.round(seconds % 60);
return m + 'min ' + (s > 0 ? s + 's' : '');
}
const h = Math.floor(seconds / 3600);
const m = Math.round((seconds % 3600) / 60);
return h + 'h ' + (m > 0 ? m + 'min' : '');
}
function escapeHtml(str) {
if (!str) return '';
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function showError(msg, detail) {
const banner = document.getElementById('errorBanner');
const text = document.getElementById('errorText');
text.textContent = msg + (detail ? ' — ' + detail : '');
banner.classList.add('visible');
}
function hideError() {
document.getElementById('errorBanner').classList.remove('visible');
}
// ========================================================================
// Init
// ========================================================================
document.addEventListener('DOMContentLoaded', loadMachines);
</script>
</body>
</html>