feat(dashboard): launch supervised competence tests

This commit is contained in:
Dom
2026-06-01 12:09:09 +02:00
parent 1a58a0d1f1
commit 335d576830
4 changed files with 476 additions and 3 deletions

View File

@@ -292,7 +292,7 @@
<th>État</th>
<th>Verdicts</th>
<th>Contextes</th>
<th>Promotion</th>
<th>Test / Promotion</th>
</tr>
</thead>
<tbody id="competencesTableBody">
@@ -335,11 +335,33 @@
</div>
</div>
<div class="modal-backdrop" id="testModal">
<div class="modal">
<div class="modal-header">
<div class="modal-title" id="testTitle">Test supervisé</div>
<button class="modal-close" onclick="closeTestModal()">Fermer</button>
</div>
<div class="modal-body">
<div id="testStatus"></div>
<h2 style="font-size:14px;margin:14px 0 8px;color:#94a3b8;">Progression</h2>
<div class="evidence-list" id="testProgress">--</div>
<h2 style="font-size:14px;margin:14px 0 8px;color:#94a3b8;">État replay</h2>
<pre class="diff-box" id="testEvidence">En attente...</pre>
<div class="danger-note">Le verdict est journalisé séparément. Aucun YAML n'est modifié par ce test.</div>
</div>
<div class="modal-actions" id="testActions">
<button class="secondary-btn" onclick="closeTestModal()">Fermer</button>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', loadKnowledgeBase);
let knowledgeBaseData = null;
let currentPromotion = null;
let currentTest = null;
let testPollTimer = null;
async function loadKnowledgeBase() {
try {
@@ -433,6 +455,7 @@ function renderCompetences(competences) {
tbody.innerHTML = items.map(item => {
const targetEntries = Object.entries(item.eligible_targets || {});
const testButton = `<button class="secondary-btn" title="Lancer un replay supervisé sans CLI" onclick="openCompetenceTest('${escapeAttr(item.id)}')">Tester</button>`;
const buttons = targetEntries.length === 0
? '<span style="color:#64748b;font-size:12px;">Aucune promotion disponible</span>'
: targetEntries.map(([target, info]) => {
@@ -462,12 +485,237 @@ function renderCompetences(competences) {
</div>
</td>
<td>${item.distinct_contexts || 0}</td>
<td><div class="action-row">${buttons}</div></td>
<td><div class="action-row">${testButton}${buttons}</div></td>
</tr>
`;
}).join('');
}
async function openCompetenceTest(competenceId) {
clearTestPolling();
currentTest = { competenceId, replayId: null, lastState: null };
document.getElementById('testModal').classList.add('visible');
document.getElementById('testTitle').textContent = `Test supervisé ${competenceId}`;
document.getElementById('testStatus').innerHTML = '<div class="alert alert-info">Lancement du replay supervisé...</div>';
document.getElementById('testProgress').textContent = '--';
document.getElementById('testEvidence').textContent = 'Initialisation...';
document.getElementById('testActions').innerHTML = '<button class="secondary-btn" onclick="closeTestModal()">Fermer</button>';
try {
const resp = await fetch(`/api/v1/lea/competences/${encodeURIComponent(competenceId)}/replay`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
supervised: true,
start_replay: true,
}),
});
const data = await resp.json();
if (!resp.ok || data.success === false) {
throw new Error(data.error || data.detail || resp.statusText);
}
const replay = data.replay || data;
const replayId = replay.replay_id;
if (!replayId) {
throw new Error('Replay lancé mais replay_id absent');
}
currentTest.replayId = replayId;
document.getElementById('testStatus').innerHTML = '<div class="alert alert-success">Replay lancé. Attente du premier état...</div>';
startTestPolling();
} catch (err) {
document.getElementById('testStatus').innerHTML = `<div class="alert alert-warning">${escapeHtml(err.message || String(err))}</div>`;
document.getElementById('testActions').innerHTML = '<button class="secondary-btn" onclick="closeTestModal()">Fermer</button>';
}
}
function startTestPolling() {
clearTestPolling();
pollCompetenceTest();
testPollTimer = setInterval(pollCompetenceTest, 1000);
}
function clearTestPolling() {
if (testPollTimer) {
clearInterval(testPollTimer);
testPollTimer = null;
}
}
async function pollCompetenceTest() {
if (!currentTest?.replayId) return;
try {
const resp = await fetch(`/api/v1/lea/replays/${encodeURIComponent(currentTest.replayId)}`);
const state = await resp.json();
if (!resp.ok || state.success === false) {
throw new Error(state.error || state.detail || resp.statusText);
}
currentTest.lastState = state;
renderTestState(state);
if (['completed', 'error', 'cancelled'].includes(state.status)) {
clearTestPolling();
if (state.status === 'completed') {
setTimeout(loadKnowledgeBase, 500);
}
}
} catch (err) {
document.getElementById('testStatus').innerHTML = `<div class="alert alert-warning">${escapeHtml(err.message || String(err))}</div>`;
}
}
function renderTestState(state) {
const completed = state.completed_actions ?? state.actions_completed ?? 0;
const total = state.total_actions ?? '?';
const remaining = state.actions_remaining ?? '';
const failed = state.failed_action || {};
const phase = state.pause_phase || failed.phase || inferPausePhase(failed.action_id || '');
const progress = [
`Replay: ${state.replay_id || currentTest?.replayId || '-'}`,
`Machine: ${state.machine_id || '-'}`,
`Statut: ${state.status || '-'}`,
`Actions: ${completed}/${total}`,
remaining !== '' ? `Restantes: ${remaining}` : '',
].filter(Boolean).join(' · ');
document.getElementById('testProgress').textContent = progress;
document.getElementById('testEvidence').textContent = JSON.stringify({
replay_id: state.replay_id || currentTest?.replayId,
status: state.status,
workflow_id: state.workflow_id,
machine_id: state.machine_id,
pause_phase: phase,
message: state.message || state.pause_message,
failed_action: failed,
results_count: Array.isArray(state.step_results || state.results) ? (state.step_results || state.results).length : 0,
}, null, 2);
if (state.status === 'paused_need_help') {
const msg = escapeHtml(state.message || state.pause_message || 'Validation humaine requise');
if (phase === 'after' || state.verdict_required || failed.verdict_required) {
document.getElementById('testStatus').innerHTML = `<div class="alert alert-info">${msg}</div>`;
document.getElementById('testActions').innerHTML = `
<button class="action-btn" onclick="submitCompetenceVerdict('valid')">Valide</button>
<button class="secondary-btn" onclick="submitCompetenceVerdict('inconclusive')">Incertain</button>
<button class="secondary-btn" onclick="submitCompetenceVerdict('invalid')">Invalide</button>
<button class="secondary-btn" onclick="closeTestModal()">Fermer</button>
`;
} else {
document.getElementById('testStatus').innerHTML = `<div class="alert alert-info">${msg}</div>`;
document.getElementById('testActions').innerHTML = `
<button class="action-btn" onclick="resumeCompetenceTest()">Continuer le test</button>
<button class="secondary-btn" onclick="closeTestModal()">Fermer</button>
`;
}
return;
}
if (state.status === 'completed') {
document.getElementById('testStatus').innerHTML = '<div class="alert alert-success">Replay terminé. La base de connaissances est rafraîchie.</div>';
document.getElementById('testActions').innerHTML = '<button class="action-btn" onclick="closeTestModal(); loadKnowledgeBase();">Fermer</button>';
return;
}
if (['error', 'cancelled'].includes(state.status)) {
document.getElementById('testStatus').innerHTML = `<div class="alert alert-warning">Replay ${escapeHtml(state.status)}.</div>`;
document.getElementById('testActions').innerHTML = '<button class="secondary-btn" onclick="closeTestModal()">Fermer</button>';
return;
}
document.getElementById('testStatus').innerHTML = '<div class="alert alert-info">Replay en cours...</div>';
document.getElementById('testActions').innerHTML = '<button class="secondary-btn" onclick="closeTestModal()">Fermer</button>';
}
function inferPausePhase(actionId) {
if (String(actionId).includes('_pause_after')) return 'after';
if (String(actionId).includes('_pause_before')) return 'before';
return '';
}
async function resumeCompetenceTest() {
if (!currentTest?.replayId) return;
document.getElementById('testStatus').innerHTML = '<div class="alert alert-info">Reprise du replay...</div>';
try {
const resp = await fetch(`/api/v1/lea/replays/${encodeURIComponent(currentTest.replayId)}/resume`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ acknowledged_check_ids: [] }),
});
const data = await resp.json();
if (!resp.ok || data.success === false) {
throw new Error(data.error || data.detail?.error || data.detail || resp.statusText);
}
startTestPolling();
} catch (err) {
document.getElementById('testStatus').innerHTML = `<div class="alert alert-warning">${escapeHtml(err.message || String(err))}</div>`;
}
}
async function submitCompetenceVerdict(kind) {
if (!currentTest?.competenceId || !currentTest?.replayId) return;
const state = currentTest.lastState || {};
const stepResults = compactStepResults(state.step_results || state.results || []);
const payload = {
verdict_id: newPromotionId(),
verdict_kind: kind,
verdict_by: 'human:dom',
workflow_id: state.workflow_id || currentTest.replayId,
step_results: stepResults,
context_signature: {
machine_id: state.machine_id || 'unknown_machine',
screen_state_initial: '',
screen_state_after_action: state.last_screenshot || '',
},
evidence: {
replay_id: currentTest.replayId,
replay_status: state.status || '',
actions_completed: state.completed_actions ?? state.actions_completed ?? 0,
total_actions: state.total_actions ?? '',
pause_phase: state.pause_phase || state.failed_action?.phase || '',
},
source: {
frontend: 'dashboard_knowledge_base',
workflow_id: state.workflow_id || currentTest.replayId,
replay_id: currentTest.replayId,
},
comments: `Verdict humain dashboard: ${kind}`,
write_back_enabled: false,
yaml_write: false,
};
try {
const resp = await fetch(`/api/v1/lea/competences/${encodeURIComponent(currentTest.competenceId)}/verdict`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload),
});
const data = await resp.json();
if (!resp.ok || !data.success) {
throw new Error(data.error || resp.statusText);
}
document.getElementById('testStatus').innerHTML = '<div class="alert alert-success">Verdict enregistré. Finalisation du replay...</div>';
await resumeCompetenceTest();
setTimeout(loadKnowledgeBase, 500);
} catch (err) {
document.getElementById('testStatus').innerHTML = `<div class="alert alert-warning">${escapeHtml(err.message || String(err))}</div>`;
}
}
function compactStepResults(results) {
if (!Array.isArray(results)) return [];
return results.map((item, index) => ({
step_id: item.step_id || item.action_id || `step_${index + 1}`,
action_type: item.action_type || item.type || item.action?.type || '',
status: item.status || item.result || (item.success === false ? 'failed' : 'success'),
error: item.error || item.message || '',
resolution_method: item.resolution_method || item.method || '',
}));
}
function closeTestModal() {
clearTestPolling();
document.getElementById('testModal').classList.remove('visible');
currentTest = null;
}
async function openPromotion(competenceId, targetState, verdictIds) {
currentPromotion = null;
document.getElementById('promotionModal').classList.add('visible');