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]');