feat(vwb): add dashboard competence testing and health tools

This commit is contained in:
Dom
2026-06-02 16:27:19 +02:00
parent d38f0b0f2f
commit 18ed6cb751
23 changed files with 2769 additions and 27 deletions

View File

@@ -76,7 +76,7 @@ _DASHBOARD_AUTH_DISABLED = os.getenv("DASHBOARD_AUTH_DISABLED", "").lower() in (
# avant un déploiement prod. On ne veut surtout pas générer un mot de passe
# aléatoire à chaque boot (même problème que l'API token auto-généré).
if not _DASHBOARD_PASSWORD and not _DASHBOARD_AUTH_DISABLED:
_DASHBOARD_PASSWORD = "changeme-dashboard-Medecin2026!"
_DASHBOARD_PASSWORD = "changeme-dashboard-RpaVision2026!"
api_logger.warning(
"[SÉCURITÉ] DASHBOARD_PASSWORD non défini en env — utilisation d'un "
"mot de passe par défaut temporaire. DÉFINIR DASHBOARD_PASSWORD "

View File

@@ -492,6 +492,9 @@ function renderCompetences(competences) {
}
async function openCompetenceTest(competenceId) {
if (!confirmRunDialogReplay(competenceId)) {
return;
}
clearTestPolling();
currentTest = { competenceId, replayId: null, lastState: null };
document.getElementById('testModal').classList.add('visible');
@@ -528,6 +531,24 @@ async function openCompetenceTest(competenceId) {
}
}
function confirmRunDialogReplay(competenceId) {
if (!mayOpenRunDialog(competenceId)) return true;
return window.confirm(
"Ce test peut ouvrir Win+R / Exécuter. Si la fenêtre Exécuter est déjà ouverte, le replay peut produire un faux positif. Fermez-la ou vérifiez l'état du poste avant de continuer."
);
}
function mayOpenRunDialog(competenceId) {
const normalized = String(competenceId || '')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase();
return normalized.includes('win_r')
|| normalized.includes('windows_r')
|| normalized.includes('executer')
|| normalized.includes('run_dialog');
}
function startTestPolling() {
clearTestPolling();
pollCompetenceTest();
@@ -568,6 +589,8 @@ function renderTestState(state) {
const remaining = state.actions_remaining ?? '';
const failed = state.failed_action || {};
const phase = state.pause_phase || failed.phase || inferPausePhase(failed.action_id || '');
const stepResults = compactStepResults(state.step_results || state.results || []);
const evidenceAvailable = hasReplayEvidence(state, stepResults);
const progress = [
`Replay: ${state.replay_id || currentTest?.replayId || '-'}`,
`Machine: ${state.machine_id || '-'}`,
@@ -585,15 +608,20 @@ function renderTestState(state) {
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,
results_count: stepResults.length,
evidence_available: evidenceAvailable,
}, 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>`;
const evidenceWarning = evidenceAvailable
? ''
: '<div class="alert alert-warning">Verdict valide bloqué : aucune step_results ni evidence replay disponible. Choisir Incertain/Invalide ou relancer avec evidence.</div>';
const validDisabled = evidenceAvailable ? '' : 'disabled title="Aucune step_results ni evidence replay disponible"';
document.getElementById('testStatus').innerHTML = `<div class="alert alert-info">${msg}</div>${evidenceWarning}`;
document.getElementById('testActions').innerHTML = `
<button class="action-btn" onclick="submitCompetenceVerdict('valid')">Valide</button>
<button class="action-btn" ${validDisabled} 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>
@@ -653,6 +681,10 @@ async function submitCompetenceVerdict(kind) {
if (!currentTest?.competenceId || !currentTest?.replayId) return;
const state = currentTest.lastState || {};
const stepResults = compactStepResults(state.step_results || state.results || []);
if (kind === 'valid' && !hasReplayEvidence(state, stepResults)) {
document.getElementById('testStatus').innerHTML = '<div class="alert alert-warning">Verdict valide refusé : aucune step_results ni evidence replay disponible.</div>';
return;
}
const payload = {
verdict_id: newPromotionId(),
verdict_kind: kind,
@@ -710,6 +742,28 @@ function compactStepResults(results) {
}));
}
function hasReplayEvidence(state, stepResults) {
if (Array.isArray(stepResults) && stepResults.length > 0) return true;
return [
state.evidence,
state.evidence_summary,
state.artifacts,
state.screenshots,
state.last_screenshot,
state.screenshot,
state.trace_path,
state.events,
].some(hasMeaningfulEvidenceValue);
}
function hasMeaningfulEvidenceValue(value) {
if (value === null || value === undefined) return false;
if (Array.isArray(value)) return value.length > 0;
if (typeof value === 'object') return Object.keys(value).length > 0;
if (typeof value === 'string') return value.trim() !== '';
return Boolean(value);
}
function closeTestModal() {
clearTestPolling();
document.getElementById('testModal').classList.remove('visible');