From 0f5296ab40b7d8a8abf7644e479367a79f622252 Mon Sep 17 00:00:00 2001 From: Admin MPCZ Date: Tue, 5 May 2026 12:28:12 +0200 Subject: [PATCH] feat(patching/iexec): terminal global HTML colore consolide - toutes les etapes (check, snap, dryrun, pre, patch, reboot, recon, post) appendent dans le meme terminal avec sections horodatees + couleurs ANSI-like + scroll auto + bouton Vider --- app/templates/patching_iexec.html | 159 +++++++++++++++++++++++++----- 1 file changed, 133 insertions(+), 26 deletions(-) diff --git a/app/templates/patching_iexec.html b/app/templates/patching_iexec.html index be9e928..b7744fc 100644 --- a/app/templates/patching_iexec.html +++ b/app/templates/patching_iexec.html @@ -94,13 +94,26 @@ {# ─── Terminal style log dry-run / patch ─── #}
@@ -166,20 +179,25 @@ async function checkOne(tr){ const rowId = tr.dataset.rowId; const osStr = tr.dataset.os || ''; + const host = tr.querySelector('td:nth-child(3)').textContent.trim(); if (!isLinux(osStr)) { tr.querySelector('.cell-dns').innerHTML = statusBadge('unsupported'); tr.querySelector('.cell-ssh').innerHTML = statusBadge('unsupported'); tr.querySelector('.cell-disk').innerHTML = statusBadge('unsupported'); tr.querySelector('.cell-sat').innerHTML = statusBadge('unsupported'); tr.querySelector('.cell-overall').innerHTML = '⊘ Windows'; + termSection('check ' + host); + termLine('⊘', 'Windows non supporté (Linux uniquement)'); return {overall: 'unsupported'}; } + termSection('check ' + host); tr.querySelector('.cell-overall').innerHTML = ''; try { const r = await fetch('/patching/iexec/check/' + rowId, {method:'POST'}); const j = await r.json(); if (!j.ok) { tr.querySelector('.cell-overall').innerHTML = 'err'; + termLine('✗', 'erreur backend'); return {overall: 'ko'}; } if (j.overall === 'unsupported') { @@ -188,19 +206,30 @@ tr.querySelector('.cell-disk').innerHTML = statusBadge('unsupported'); tr.querySelector('.cell-sat').innerHTML = statusBadge('unsupported'); tr.querySelector('.cell-overall').innerHTML = '⊘ N/A'; + termLine('⊘', j.skipped_reason || 'non supporté'); return j; } + appendTerm(' # target=' + (j.target || '?') + '\n'); const byName = {}; - (j.checks || []).forEach(c => byName[c.name] = c); + (j.checks || []).forEach(c => { + byName[c.name] = c; + const sym = c.status === 'ok' ? '✓' : (c.status === 'warn' ? '⚠' : '✗'); + termLine(sym, c.label + ' : ' + c.message); + if (c.details && c.status !== 'ok') { + appendTerm(' │ ' + (c.details || '').split('\n').slice(0,8).join('\n │ ') + '\n'); + } + }); tr.querySelector('.cell-dns').innerHTML = byName.dns ? statusBadge(byName.dns.status) : '–'; tr.querySelector('.cell-ssh').innerHTML = byName.ssh ? statusBadge(byName.ssh.status) : '–'; tr.querySelector('.cell-disk').innerHTML = byName.disk ? statusBadge(byName.disk.status) + ' ' + escapeHTML(byName.disk.message) + '' : '–'; tr.querySelector('.cell-sat').innerHTML = byName.satellite ? statusBadge(byName.satellite.status) : '–'; tr.querySelector('.cell-overall').innerHTML = statusBadge(j.overall) + ' (' + (j.duration_ms||0) + 'ms)'; + termLine('→', 'verdict ' + j.overall + ' (' + (j.duration_ms||0) + 'ms)'); tr._checkData = j; return j; } catch(e) { tr.querySelector('.cell-overall').innerHTML = 'err'; + termLine('✗', 'exception : ' + e.message); return {overall: 'ko'}; } } @@ -222,29 +251,65 @@ btnStep2.disabled = (okCount === 0); }); - // ─── Terminal ─── + // ─── Terminal global (toutes les étapes) ─── const termCard = document.getElementById('term-card'); const termTitle = document.getElementById('term-title'); const termPane = document.getElementById('term-pane'); document.getElementById('term-close').addEventListener('click', () => { termCard.style.display = 'none'; }); - function openTerm(title){ - termCard.style.display = ''; - termTitle.textContent = '$ ' + title; - termPane.textContent = ''; - termCard.scrollIntoView({behavior:'smooth', block:'nearest'}); + document.getElementById('term-clear').addEventListener('click', () => { + termPane.innerHTML = ''; + }); + function showTerm(){ + if (termCard.style.display === 'none') { + termCard.style.display = ''; + termCard.scrollIntoView({behavior:'smooth', block:'nearest'}); + } + } + function appendTermHTML(html){ + termPane.insertAdjacentHTML('beforeend', html); + termPane.scrollTop = termPane.scrollHeight; } function appendTerm(s){ - termPane.textContent += s; - termPane.scrollTop = termPane.scrollHeight; + // Compat : ancien API qui écrit du texte brut + appendTermHTML(escapeHTML(s)); + } + function termSection(label){ + showTerm(); + const ts = new Date().toLocaleTimeString('fr-FR', {hour12: false}); + appendTermHTML('\n══════ [' + escapeHTML(label) + '] ' + ts + ' ══════\n'); + } + function termLine(prefix, text){ + let cls = ''; + if (prefix === '✓') cls = 't-ok'; + else if (prefix === '⚠') cls = 't-warn'; + else if (prefix === '✗') cls = 't-ko'; + else if (prefix === '⊘') cls = 't-mute'; + else if (prefix === '⏳') cls = 't-warn'; + else if (prefix === '→') cls = 't-info'; + else if (prefix === '#') cls = 't-mute'; + appendTermHTML(' ' + escapeHTML(prefix + ' ' + text) + '\n'); + } + function termDetail(text){ + if (!text) return; + const lines = String(text).split('\n').slice(0, 12); + appendTermHTML('' + escapeHTML(lines.join('\n')) + '\n'); + } + function classifyYumLine(s){ + const ll = String(s).toLowerCase(); + if (ll.startsWith('error') || ll.startsWith('erreur') || ll.includes(' fail') || ll.includes('failed')) return 't-ko'; + if (ll.startsWith('warning') || ll.startsWith('attention')) return 't-warn'; + if (ll.includes('complete!') || ll.includes('terminé !') || ll.includes('nothing to do') || ll.includes('rien à faire')) return 't-ok'; + if (ll.startsWith('==') || ll.startsWith('--') || ll.startsWith('transaction')) return 't-info'; + return ''; } function streamYum(rowId, mode, hostname, extraExcludes){ return new Promise((resolve) => { - const titre = (mode === 'dryrun' ? 'dry-run' : 'PATCH') + ' yum @ ' + hostname + const titre = (mode === 'dryrun' ? 'dry-run' : 'PATCH') + ' yum ' + hostname + (extraExcludes ? ' [retry +' + extraExcludes + ']' : ''); - openTerm(titre); + termSection(titre); let url = '/patching/iexec/yum-stream/' + rowId + '?mode=' + mode; if (extraExcludes) url += '&extra_excludes=' + encodeURIComponent(extraExcludes); const ev = new EventSource(url); @@ -253,11 +318,13 @@ let j; try { j = JSON.parse(m.data); } catch(e) { return; } if (j.type === 'cmd') { - appendTerm('# host : ' + (j.hostname||'') + ' (' + (j.target||'') + ')\n'); - appendTerm('# cmd : ' + (j.cmd||'') + '\n'); - appendTerm('# excludes (' + (j.excludes||[]).length + ')\n\n'); + appendTerm(' # host : ' + (j.hostname||'') + ' (' + (j.target||'') + ')\n'); + appendTerm(' # cmd : ' + (j.cmd||'') + '\n'); + appendTerm(' # excludes (' + (j.excludes||[]).length + ')\n'); } else if (j.type === 'line') { - appendTerm(j.data + '\n'); + const cls = classifyYumLine(j.data); + if (cls) appendTermHTML(' ' + escapeHTML(j.data) + '\n'); + else appendTerm(j.data + '\n'); result.lines++; const ll = j.data.toLowerCase(); if (ll.includes('package') || ll.includes('paquet') @@ -353,6 +420,8 @@ btnRun.disabled = true; let okCount = 0, koCount = 0; for (const tr of okTrs) { + const host = tr.querySelector('td:nth-child(3)').textContent.trim(); + termSection('snapshot ' + host); const cell = tr.querySelector('.cell-snap'); cell.innerHTML = '… snapshot'; try { @@ -362,17 +431,22 @@ okCount++; cell.innerHTML = '✓ ' + escapeHTML(j.snap_name||'OK') + '' + ' (' + escapeHTML(j.vcenter||'') + ')'; + termLine('✓', 'snap_name=' + (j.snap_name||'') + ' on vCenter ' + (j.vcenter||'')); + if (j.branch) appendTerm(' # branch=' + j.branch + ' vm=' + (j.vm_name||'') + '\n'); } else if (j.skipped) { koCount++; cell.innerHTML = '⚠ ' + escapeHTML(j.detail||'skip') + ''; + termLine('⚠', j.detail || 'skipped'); } else { koCount++; cell.innerHTML = '✗ ' + escapeHTML((j.detail||'KO').slice(0,80)) + ''; + termLine('✗', j.detail || 'KO'); } tr._snapData = j; } catch(e) { koCount++; cell.innerHTML = '✗ erreur réseau'; + termLine('✗', 'exception : ' + e.message); } } btnRun.disabled = false; @@ -407,6 +481,8 @@ btnPre.disabled = true; btnStep3.disabled = true; let okCount = 0, koCount = 0; for (const tr of targets) { + const host = tr.querySelector('td:nth-child(3)').textContent.trim(); + termSection('pre-capture ' + host); const cell = tr.querySelector('.cell-pre'); cell.innerHTML = '… capture'; try { @@ -416,13 +492,18 @@ if (j.ok) { okCount++; cell.innerHTML = '✓ snapshot'; + termLine('✓', 'services + ports capturés dans /tmp/secops_*_avant_*.txt'); + termDetail(j.stdout); } else { koCount++; cell.innerHTML = '✗ ' + escapeHTML((j.detail||'KO').slice(0,80)) + ''; + termLine('✗', j.detail || 'KO'); + if (j.stderr) termDetail(j.stderr); } } catch(e) { koCount++; cell.innerHTML = '✗ erreur'; + termLine('✗', 'exception : ' + e.message); } } summary.innerHTML += ' · Pre-capt : ✓ ' + okCount + ' / ✗ ' + koCount; @@ -438,6 +519,8 @@ btnReboot.disabled = true; let okCount = 0, koCount = 0; for (const tr of targets) { + const host = tr.querySelector('td:nth-child(3)').textContent.trim(); + termSection('reboot ' + host); const cell = tr.querySelector('.cell-recon'); cell.innerHTML = '… reboot demandé'; try { @@ -447,13 +530,17 @@ if (j.ok) { okCount++; cell.innerHTML = '⏳ reboot dans 1min · ' + escapeHTML(j.started_at||'') + ''; + termLine('⏳', 'shutdown -r +1 envoyé · effectif dans 1 min · ' + (j.started_at||'')); } else { koCount++; cell.innerHTML = '✗ ' + escapeHTML((j.detail||'KO').slice(0,80)) + ''; + termLine('✗', j.detail || 'KO'); + if (j.stderr) termDetail(j.stderr); } } catch(e) { koCount++; cell.innerHTML = '✗ erreur'; + termLine('✗', 'exception : ' + e.message); } } summary.innerHTML += ' · Reboot : ✓ ' + okCount + ' / ✗ ' + koCount; @@ -468,11 +555,14 @@ const startTs = Date.now(); // Pour chaque target, polling indépendant await Promise.all(targets.map(async (tr) => { + const host = tr.querySelector('td:nth-child(3)').textContent.trim(); + termSection('wait reconnexion ' + host); const cell = tr.querySelector('.cell-recon'); const t0 = Date.now(); - const TIMEOUT_MS = 10 * 60 * 1000; // 10 min + const TIMEOUT_MS = 10 * 60 * 1000; const POLL_MS = 10 * 1000; cell.innerHTML = '⏳ poll TCP/22…'; + let last = ''; while (Date.now() - t0 < TIMEOUT_MS) { await new Promise(r => setTimeout(r, POLL_MS)); try { @@ -483,15 +573,21 @@ tr._reconData = {ok: true, downtime_s: dur, uptime: j.uptime}; cell.innerHTML = '✓ revenu en ' + dur + 's' + '
' + escapeHTML((j.uptime||'').slice(0,60)) + ''; + termLine('✓', host + ' revenu en ' + dur + 's · ' + (j.uptime||'')); return; } - cell.innerHTML = '⏳ ' - + (j.tcp22 ? 'TCP/22 OK · SSH KO' : 'pas joignable') - + ' · ' + Math.round((Date.now()-t0)/1000) + 's'; - } catch(e) { /* ignore, retry */ } + const status = j.tcp22 ? 'TCP/22 OK · SSH KO' : 'pas joignable'; + const elapsed = Math.round((Date.now()-t0)/1000); + cell.innerHTML = '⏳ ' + status + ' · ' + elapsed + 's'; + if (status !== last) { + termLine('⏳', host + ' : ' + status + ' (t+' + elapsed + 's)'); + last = status; + } + } catch(e) { /* retry */ } } tr._reconData = {ok: false}; cell.innerHTML = '✗ timeout 10 min'; + termLine('✗', host + ' : timeout 10 min sans reconnexion'); })); refreshStepButtons(); }); @@ -504,6 +600,8 @@ btnPost.disabled = true; let okCount = 0, warnCount = 0, koCount = 0; for (const tr of targets) { + const host = tr.querySelector('td:nth-child(3)').textContent.trim(); + termSection('post-compare ' + host); const cell = tr.querySelector('.cell-post'); cell.innerHTML = '… compare'; try { @@ -517,12 +615,21 @@ const dispPort = (rep.ports_disparus||[]).length; const appPort = (rep.ports_apparus||[]).length; const summ = 'svc -' + dispSvc + ' +' + appSvc + ' / port -' + dispPort + ' +' + appPort; - if (st === 'ok') { okCount++; cell.innerHTML = '✓ ' + escapeHTML(summ) + ''; } - else if (st === 'warn') { warnCount++; cell.innerHTML = '⚠ ' + escapeHTML(summ) + ''; } - else { koCount++; cell.innerHTML = '✗ ' + escapeHTML(summ) + ''; } + if (st === 'ok') { okCount++; cell.innerHTML = '✓ ' + escapeHTML(summ) + ''; termLine('✓', summ); } + else if (st === 'warn') { warnCount++; cell.innerHTML = '⚠ ' + escapeHTML(summ) + ''; termLine('⚠', summ); } + else { koCount++; cell.innerHTML = '✗ ' + escapeHTML(summ) + ''; termLine('✗', summ); } + if (rep.services_disparus && rep.services_disparus.length) + appendTermHTML(' services disparus : ' + escapeHTML(rep.services_disparus.join(', ')) + '\n'); + if (rep.services_apparus && rep.services_apparus.length) + appendTermHTML(' services apparus : ' + escapeHTML(rep.services_apparus.join(', ')) + '\n'); + if (rep.ports_disparus && rep.ports_disparus.length) + appendTermHTML(' ports disparus : ' + escapeHTML(rep.ports_disparus.join(', ')) + '\n'); + if (rep.ports_apparus && rep.ports_apparus.length) + appendTermHTML(' ports apparus : ' + escapeHTML(rep.ports_apparus.join(', ')) + '\n'); } catch(e) { koCount++; cell.innerHTML = '✗ erreur'; + termLine('✗', 'exception : ' + e.message); } } summary.innerHTML += ' · Post-cmp : ✓ ' + okCount + ' · ⚠ ' + warnCount + ' · ✗ ' + koCount;