From 830eaaa5196e74ca90f9c62705bcdb33ba3123e6 Mon Sep 17 00:00:00 2001 From: Admin MPCZ Date: Tue, 5 May 2026 12:58:39 +0200 Subject: [PATCH] feat(patching/iexec): boutons et stepper avec etats visuels (gris pending / orange en cours / vert done / rouge failed) - cascade automatique selon resultats accumules + animation pulse pour running --- app/templates/patching_iexec.html | 193 ++++++++++++++++++++++-------- 1 file changed, 140 insertions(+), 53 deletions(-) diff --git a/app/templates/patching_iexec.html b/app/templates/patching_iexec.html index b7744fc..99214e2 100644 --- a/app/templates/patching_iexec.html +++ b/app/templates/patching_iexec.html @@ -13,19 +13,44 @@ {# ─── Stepper ─── #} -
- 1. Vérifications - - 2. Snapshot - - 3. Patch yum + +
+ 1 Vérif + + 2 Snapshot + + 3a Dry-run + + 3b Pre-capt + + 3c Patch + + 3e Reboot + + 3f Reconn. + + 3g Post-cmp

Step 1 — Vérifications pré-patching

- +

@@ -116,30 +141,35 @@

+
- - - - - - - + + + + + + +
@@ -235,7 +265,7 @@ } btnRun.addEventListener('click', async () => { - btnRun.disabled = true; btnStep2.disabled = true; + setBtnState(btnRun, 'running'); setStepState('check', 'running'); summary.textContent = 'Lancement…'; const trs = Array.from(tbody.querySelectorAll('tr[data-row-id]')); let okCount = 0, warnCount = 0, koCount = 0, naCount = 0; @@ -247,8 +277,7 @@ else koCount++; } summary.innerHTML = '✓ ' + okCount + ' OK · ⚠ ' + warnCount + ' warn · ✗ ' + koCount + ' KO · ⊘ ' + naCount + ' N/A'; - btnRun.disabled = false; - btnStep2.disabled = (okCount === 0); + refreshStepButtons(); }); // ─── Terminal global (toutes les étapes) ─── @@ -394,21 +423,77 @@ refreshStepButtons(); } + // ─── États visuels boutons + stepper ─── + function setBtnState(btn, state){ + if (!btn) return; + btn.classList.remove('s-pending','s-current','s-done','s-failed','s-running'); + btn.classList.add('s-' + state); + btn.disabled = (state === 'pending' || state === 'running'); + } + function setStepState(name, state){ + const el = document.querySelector('#stepper [data-step="' + name + '"]'); + if (!el) return; + el.classList.remove('s-pending','s-current','s-done','s-failed','s-running'); + el.classList.add('s-' + state); + } + + // Calcul de l'état de chaque étape selon les données accumulées + function deriveState(prevDoneCount, ownAttempted, ownOkCount){ + // pending : étape précédente pas encore validée + if (prevDoneCount === 0) return 'pending'; + // pas encore tenté : étape actuelle (à faire) + if (!ownAttempted) return 'current'; + // tenté : ok si au moins 1 OK, failed sinon + return ownOkCount > 0 ? 'done' : 'failed'; + } + function refreshStepButtons(){ const trs = Array.from(tbody.querySelectorAll('tr[data-row-id]')); - const ckOk = trs.filter(tr => tr._checkData && tr._checkData.overall === 'ok'); - const snapOk = trs.filter(tr => tr._snapData && tr._snapData.ok); - const dryOk = trs.filter(tr => tr._dryData && tr._dryData.ok); - const preOk = trs.filter(tr => tr._preData && tr._preData.ok); - const patchOk = trs.filter(tr => tr._patchData && tr._patchData.ok); - const recOk = trs.filter(tr => tr._reconData && tr._reconData.ok); - btnStep2.disabled = (ckOk.length === 0); - btnDryrun.disabled = (snapOk.length === 0); - btnPre.disabled = (dryOk.length === 0); - btnStep3.disabled = (preOk.length === 0); - btnReboot.disabled = (patchOk.length === 0); - btnRecon.disabled = (patchOk.length === 0); - btnPost.disabled = (recOk.length === 0 && patchOk.length === 0); + const checkAttempted = trs.some(tr => tr._checkData); + const ckOk = trs.filter(tr => tr._checkData && tr._checkData.overall === 'ok').length; + const snapAttempted = trs.some(tr => tr._snapData); + const snapOk = trs.filter(tr => tr._snapData && tr._snapData.ok).length; + const dryAttempted = trs.some(tr => tr._dryData); + const dryOk = trs.filter(tr => tr._dryData && tr._dryData.ok).length; + const preAttempted = trs.some(tr => tr._preData); + const preOk = trs.filter(tr => tr._preData && tr._preData.ok).length; + const patchAttempted = trs.some(tr => tr._patchData); + const patchOk = trs.filter(tr => tr._patchData && tr._patchData.ok).length; + const rebootAttempted = trs.some(tr => tr._rebootData); + const rebootOk = trs.filter(tr => tr._rebootData && tr._rebootData.ok).length; + const reconAttempted = trs.some(tr => tr._reconData); + const reconOk = trs.filter(tr => tr._reconData && tr._reconData.ok).length; + const postAttempted = trs.some(tr => tr._postData); + const postOk = trs.filter(tr => tr._postData && tr._postData.ok).length; + + // Step 1 (check) : toujours dispo. current si pas tenté, done si OK, failed si tenté KO + const checkState = !checkAttempted ? 'current' : (ckOk > 0 ? 'done' : 'failed'); + setBtnState(btnRun, checkState); + setStepState('check', checkState); + + // Cascade : chaque étape dépend du `done` de la précédente + const snapState = deriveState(ckOk, snapAttempted, snapOk); + setBtnState(btnStep2, snapState); setStepState('snap', snapState); + + const dryState = deriveState(snapOk, dryAttempted, dryOk); + setBtnState(btnDryrun, dryState); setStepState('dry', dryState); + + const preState = deriveState(dryOk, preAttempted, preOk); + setBtnState(btnPre, preState); setStepState('pre', preState); + + const patchState = deriveState(preOk, patchAttempted, patchOk); + setBtnState(btnStep3, patchState); setStepState('patch', patchState); + + const rebootState = deriveState(patchOk, rebootAttempted, rebootOk); + setBtnState(btnReboot, rebootState); setStepState('reboot', rebootState); + + const reconState = deriveState(patchOk, reconAttempted, reconOk); + setBtnState(btnRecon, reconState); setStepState('recon', reconState); + + // Post-cmp dispo dès qu'on a soit recon OK (idéal après reboot) soit patch OK + const postPrevDone = Math.max(reconOk, patchOk); + const postState = deriveState(postPrevDone, postAttempted, postOk); + setBtnState(btnPost, postState); setStepState('post', postState); } btnStep2.addEventListener('click', async () => { @@ -416,8 +501,7 @@ const okTrs = trs.filter(tr => tr._checkData && tr._checkData.overall === 'ok'); if (!okTrs.length) { alert('Aucune ligne avec verdict OK'); return; } if (!confirm('Lancer snapshot vCenter pour ' + okTrs.length + ' serveur(s) ?\n(nom = _YYYY-MM-DD_avant_patch)')) return; - btnStep2.disabled = true; - btnRun.disabled = true; + setBtnState(btnStep2, 'running'); setStepState('snap', 'running'); let okCount = 0, koCount = 0; for (const tr of okTrs) { const host = tr.querySelector('td:nth-child(3)').textContent.trim(); @@ -449,7 +533,6 @@ termLine('✗', 'exception : ' + e.message); } } - btnRun.disabled = false; summary.innerHTML += ' · Snapshot : ✓ ' + okCount + ' / ✗ ' + koCount; refreshStepButtons(); }); @@ -459,7 +542,7 @@ const targets = trs.filter(tr => tr._snapData && tr._snapData.ok); if (!targets.length) { alert('Aucun serveur avec snapshot OK'); return; } if (!confirm('Lancer dry-run yum (simulation) sur ' + targets.length + ' serveur(s) ?\nLog en temps réel dans le terminal.')) return; - btnDryrun.disabled = true; btnStep3.disabled = true; + setBtnState(btnDryrun, 'running'); setStepState('dry', 'running'); let okCount = 0, koCount = 0; for (const tr of targets) { const host = tr.querySelector('td:nth-child(2)').textContent.trim() @@ -478,7 +561,7 @@ const targets = trs.filter(tr => tr._dryData && tr._dryData.ok); if (!targets.length) { alert('Aucun serveur avec dry-run OK'); return; } if (!confirm('Capture services+ports avant patch sur ' + targets.length + ' serveur(s) ?')) return; - btnPre.disabled = true; btnStep3.disabled = true; + setBtnState(btnPre, 'running'); setStepState('pre', 'running'); let okCount = 0, koCount = 0; for (const tr of targets) { const host = tr.querySelector('td:nth-child(3)').textContent.trim(); @@ -516,7 +599,7 @@ if (!targets.length) { alert('Aucun serveur avec patch OK'); return; } if (!confirm('⚠ REBOOT ⚠\n\nDéclencher `shutdown -r +1` sur ' + targets.length + ' serveur(s) ?\n(le reboot effectif a lieu dans 1 minute)')) return; if (!confirm('Vraiment ? Liste des hôtes :\n' + targets.map(tr => tr.querySelector('td:nth-child(3)').textContent.trim()).join('\n') + '\n\nConfirmer le reboot ?')) return; - btnReboot.disabled = true; + setBtnState(btnReboot, 'running'); setStepState('reboot', 'running'); let okCount = 0, koCount = 0; for (const tr of targets) { const host = tr.querySelector('td:nth-child(3)').textContent.trim(); @@ -544,6 +627,7 @@ } } summary.innerHTML += ' · Reboot : ✓ ' + okCount + ' / ✗ ' + koCount; + refreshStepButtons(); }); btnRecon.addEventListener('click', async () => { @@ -551,7 +635,7 @@ const targets = trs.filter(tr => tr._patchData && tr._patchData.ok); if (!targets.length) { alert('Aucun serveur avec patch OK'); return; } if (!confirm('Attendre la reconnexion (TCP/22 + SSH) sur ' + targets.length + ' serveur(s) ?\nPoll toutes les 10s, timeout 10 min par serveur.')) return; - btnRecon.disabled = true; + setBtnState(btnRecon, 'running'); setStepState('recon', 'running'); const startTs = Date.now(); // Pour chaque target, polling indépendant await Promise.all(targets.map(async (tr) => { @@ -597,7 +681,7 @@ const targets = trs.filter(tr => tr._patchData && tr._patchData.ok); if (!targets.length) { alert('Aucun serveur avec patch OK'); return; } if (!confirm('Comparer services+ports avant/après patch sur ' + targets.length + ' serveur(s) ?\n(à lancer après le reboot du serveur)')) return; - btnPost.disabled = true; + setBtnState(btnPost, 'running'); setStepState('post', 'running'); let okCount = 0, warnCount = 0, koCount = 0; for (const tr of targets) { const host = tr.querySelector('td:nth-child(3)').textContent.trim(); @@ -642,7 +726,7 @@ if (!targets.length) { alert('Aucun serveur avec pre-capture OK'); return; } if (!confirm('⚠ ATTENTION ⚠\n\nLancer yum update -y (PATCH RÉEL) sur ' + targets.length + ' serveur(s) ?\nSnapshot + pre-capture déjà faits.\nLog en temps réel dans le terminal.')) return; if (!confirm('Confirmer une 2e fois : patcher RÉELLEMENT ' + targets.length + ' serveur(s) ?')) return; - btnStep3.disabled = true; btnDryrun.disabled = true; btnStep2.disabled = true; + setBtnState(btnStep3, 'running'); setStepState('patch', 'running'); let okCount = 0, koCount = 0; for (const tr of targets) { const host = tr.querySelector('td:nth-child(2)').textContent.trim() @@ -656,6 +740,9 @@ refreshStepButtons(); }); + // Init états visuels au chargement + refreshStepButtons(); + // Click sur une ligne → afficher les détails tbody.addEventListener('click', (ev) => { const tr = ev.target.closest('tr[data-row-id]');