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
This commit is contained in:
parent
ff95424e03
commit
0f5296ab40
@ -94,13 +94,26 @@
|
||||
{# ─── Terminal style log dry-run / patch ─── #}
|
||||
<div class="card p-2 mb-4" id="term-card" style="display:none;">
|
||||
<div class="flex justify-between items-center mb-1">
|
||||
<h3 id="term-title" class="text-xs text-cyber-accent font-bold font-mono">$ terminal</h3>
|
||||
<button id="term-close" class="text-xs text-gray-500 hover:text-cyber-accent">✕ Fermer</button>
|
||||
<h3 id="term-title" class="text-xs text-cyber-accent font-bold font-mono">$ patchcenter iexec — log</h3>
|
||||
<div class="flex gap-2">
|
||||
<button id="term-clear" class="text-xs text-gray-500 hover:text-cyber-accent">🗑 Vider</button>
|
||||
<button id="term-close" class="text-xs text-gray-500 hover:text-cyber-accent">✕ Masquer</button>
|
||||
</div>
|
||||
</div>
|
||||
<pre id="term-pane" class="p-3 text-[11px] whitespace-pre-wrap overflow-auto"
|
||||
style="max-height:500px; background:#0a0e14; color:#a6e22e;
|
||||
style="max-height:600px; background:#0a0e14; color:#cdd6f4;
|
||||
font-family:'Cascadia Code','Consolas','Courier New',monospace;
|
||||
border:1px solid #1f2937; line-height:1.4;"></pre>
|
||||
<style>
|
||||
#term-pane .t-section { color:#c678dd; font-weight:bold; }
|
||||
#term-pane .t-ok { color:#a6e22e; }
|
||||
#term-pane .t-warn { color:#e5c07b; }
|
||||
#term-pane .t-ko { color:#ff5555; }
|
||||
#term-pane .t-info { color:#61afef; }
|
||||
#term-pane .t-mute { color:#7f848e; }
|
||||
#term-pane .t-cmd { color:#56b6c2; font-weight:bold; }
|
||||
#term-pane .t-detail { color:#abb2bf; padding-left:2ch; display:block; }
|
||||
</style>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center mt-4 flex-wrap gap-2">
|
||||
@ -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 = '<span class="text-gray-400" title="Workflow Linux uniquement">⊘ Windows</span>';
|
||||
termSection('check ' + host);
|
||||
termLine('⊘', 'Windows non supporté (Linux uniquement)');
|
||||
return {overall: 'unsupported'};
|
||||
}
|
||||
termSection('check ' + host);
|
||||
tr.querySelector('.cell-overall').innerHTML = '<span class="text-cyber-yellow">…</span>';
|
||||
try {
|
||||
const r = await fetch('/patching/iexec/check/' + rowId, {method:'POST'});
|
||||
const j = await r.json();
|
||||
if (!j.ok) {
|
||||
tr.querySelector('.cell-overall').innerHTML = '<span class="text-cyber-red">err</span>';
|
||||
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 = '<span class="text-gray-400" title="' + (j.skipped_reason||'') + '">⊘ N/A</span>';
|
||||
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) + ' <span class="text-[10px] text-gray-400">' + escapeHTML(byName.disk.message) + '</span>' : '–';
|
||||
tr.querySelector('.cell-sat').innerHTML = byName.satellite ? statusBadge(byName.satellite.status) : '–';
|
||||
tr.querySelector('.cell-overall').innerHTML = statusBadge(j.overall) + ' <span class="text-[10px] text-gray-500">(' + (j.duration_ms||0) + 'ms)</span>';
|
||||
termLine('→', 'verdict ' + j.overall + ' (' + (j.duration_ms||0) + 'ms)');
|
||||
tr._checkData = j;
|
||||
return j;
|
||||
} catch(e) {
|
||||
tr.querySelector('.cell-overall').innerHTML = '<span class="text-cyber-red">err</span>';
|
||||
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){
|
||||
document.getElementById('term-clear').addEventListener('click', () => {
|
||||
termPane.innerHTML = '';
|
||||
});
|
||||
function showTerm(){
|
||||
if (termCard.style.display === 'none') {
|
||||
termCard.style.display = '';
|
||||
termTitle.textContent = '$ ' + title;
|
||||
termPane.textContent = '';
|
||||
termCard.scrollIntoView({behavior:'smooth', block:'nearest'});
|
||||
}
|
||||
function appendTerm(s){
|
||||
termPane.textContent += s;
|
||||
}
|
||||
function appendTermHTML(html){
|
||||
termPane.insertAdjacentHTML('beforeend', html);
|
||||
termPane.scrollTop = termPane.scrollHeight;
|
||||
}
|
||||
function appendTerm(s){
|
||||
// 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<span class="t-section">══════ [' + escapeHTML(label) + '] ' + ts + ' ══════</span>\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(' <span class="' + cls + '">' + escapeHTML(prefix + ' ' + text) + '</span>\n');
|
||||
}
|
||||
function termDetail(text){
|
||||
if (!text) return;
|
||||
const lines = String(text).split('\n').slice(0, 12);
|
||||
appendTermHTML('<span class="t-detail">' + escapeHTML(lines.join('\n')) + '</span>\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);
|
||||
@ -255,9 +320,11 @@
|
||||
if (j.type === 'cmd') {
|
||||
appendTerm(' # host : ' + (j.hostname||'') + ' (' + (j.target||'') + ')\n');
|
||||
appendTerm(' # cmd : ' + (j.cmd||'') + '\n');
|
||||
appendTerm('# excludes (' + (j.excludes||[]).length + ')\n\n');
|
||||
appendTerm(' # excludes (' + (j.excludes||[]).length + ')\n');
|
||||
} else if (j.type === 'line') {
|
||||
appendTerm(j.data + '\n');
|
||||
const cls = classifyYumLine(j.data);
|
||||
if (cls) appendTermHTML(' <span class="' + cls + '">' + escapeHTML(j.data) + '</span>\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 = '<span class="text-cyber-yellow">… snapshot</span>';
|
||||
try {
|
||||
@ -362,17 +431,22 @@
|
||||
okCount++;
|
||||
cell.innerHTML = '<span class="text-cyber-green" title="' + escapeHTML(j.detail||'') + '">✓ ' + escapeHTML(j.snap_name||'OK') + '</span>'
|
||||
+ ' <span class="text-[10px] text-gray-400">(' + escapeHTML(j.vcenter||'') + ')</span>';
|
||||
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 = '<span class="text-cyber-yellow">⚠ ' + escapeHTML(j.detail||'skip') + '</span>';
|
||||
termLine('⚠', j.detail || 'skipped');
|
||||
} else {
|
||||
koCount++;
|
||||
cell.innerHTML = '<span class="text-cyber-red" title="' + escapeHTML(j.detail||'') + '">✗ ' + escapeHTML((j.detail||'KO').slice(0,80)) + '</span>';
|
||||
termLine('✗', j.detail || 'KO');
|
||||
}
|
||||
tr._snapData = j;
|
||||
} catch(e) {
|
||||
koCount++;
|
||||
cell.innerHTML = '<span class="text-cyber-red">✗ erreur réseau</span>';
|
||||
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 = '<span class="text-cyber-yellow">… capture</span>';
|
||||
try {
|
||||
@ -416,13 +492,18 @@
|
||||
if (j.ok) {
|
||||
okCount++;
|
||||
cell.innerHTML = '<span class="text-cyber-green" title="' + escapeHTML(j.stdout||'') + '">✓ snapshot</span>';
|
||||
termLine('✓', 'services + ports capturés dans /tmp/secops_*_avant_*.txt');
|
||||
termDetail(j.stdout);
|
||||
} else {
|
||||
koCount++;
|
||||
cell.innerHTML = '<span class="text-cyber-red" title="' + escapeHTML(j.detail||j.stderr||'') + '">✗ ' + escapeHTML((j.detail||'KO').slice(0,80)) + '</span>';
|
||||
termLine('✗', j.detail || 'KO');
|
||||
if (j.stderr) termDetail(j.stderr);
|
||||
}
|
||||
} catch(e) {
|
||||
koCount++;
|
||||
cell.innerHTML = '<span class="text-cyber-red">✗ erreur</span>';
|
||||
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 = '<span class="text-cyber-yellow">… reboot demandé</span>';
|
||||
try {
|
||||
@ -447,13 +530,17 @@
|
||||
if (j.ok) {
|
||||
okCount++;
|
||||
cell.innerHTML = '<span class="text-cyber-yellow">⏳ reboot dans 1min · ' + escapeHTML(j.started_at||'') + '</span>';
|
||||
termLine('⏳', 'shutdown -r +1 envoyé · effectif dans 1 min · ' + (j.started_at||''));
|
||||
} else {
|
||||
koCount++;
|
||||
cell.innerHTML = '<span class="text-cyber-red" title="' + escapeHTML(j.detail||j.stderr||'') + '">✗ ' + escapeHTML((j.detail||'KO').slice(0,80)) + '</span>';
|
||||
termLine('✗', j.detail || 'KO');
|
||||
if (j.stderr) termDetail(j.stderr);
|
||||
}
|
||||
} catch(e) {
|
||||
koCount++;
|
||||
cell.innerHTML = '<span class="text-cyber-red">✗ erreur</span>';
|
||||
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 = '<span class="text-cyber-yellow">⏳ poll TCP/22…</span>';
|
||||
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 = '<span class="text-cyber-green">✓ revenu en ' + dur + 's</span>'
|
||||
+ '<br><span class="text-[10px] text-gray-400" title="' + escapeHTML(j.uptime||'') + '">' + escapeHTML((j.uptime||'').slice(0,60)) + '</span>';
|
||||
termLine('✓', host + ' revenu en ' + dur + 's · ' + (j.uptime||''));
|
||||
return;
|
||||
}
|
||||
cell.innerHTML = '<span class="text-cyber-yellow">⏳ '
|
||||
+ (j.tcp22 ? 'TCP/22 OK · SSH KO' : 'pas joignable')
|
||||
+ ' · ' + Math.round((Date.now()-t0)/1000) + 's</span>';
|
||||
} 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 = '<span class="text-cyber-yellow">⏳ ' + status + ' · ' + elapsed + 's</span>';
|
||||
if (status !== last) {
|
||||
termLine('⏳', host + ' : ' + status + ' (t+' + elapsed + 's)');
|
||||
last = status;
|
||||
}
|
||||
} catch(e) { /* retry */ }
|
||||
}
|
||||
tr._reconData = {ok: false};
|
||||
cell.innerHTML = '<span class="text-cyber-red">✗ timeout 10 min</span>';
|
||||
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 = '<span class="text-cyber-yellow">… compare</span>';
|
||||
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 = '<span class="text-cyber-green">✓ ' + escapeHTML(summ) + '</span>'; }
|
||||
else if (st === 'warn') { warnCount++; cell.innerHTML = '<span class="text-cyber-yellow" title="' + escapeHTML(j.stdout||'') + '">⚠ ' + escapeHTML(summ) + '</span>'; }
|
||||
else { koCount++; cell.innerHTML = '<span class="text-cyber-red" title="' + escapeHTML(j.stdout||j.detail||'') + '">✗ ' + escapeHTML(summ) + '</span>'; }
|
||||
if (st === 'ok') { okCount++; cell.innerHTML = '<span class="text-cyber-green">✓ ' + escapeHTML(summ) + '</span>'; termLine('✓', summ); }
|
||||
else if (st === 'warn') { warnCount++; cell.innerHTML = '<span class="text-cyber-yellow" title="' + escapeHTML(j.stdout||'') + '">⚠ ' + escapeHTML(summ) + '</span>'; termLine('⚠', summ); }
|
||||
else { koCount++; cell.innerHTML = '<span class="text-cyber-red" title="' + escapeHTML(j.stdout||j.detail||'') + '">✗ ' + escapeHTML(summ) + '</span>'; termLine('✗', summ); }
|
||||
if (rep.services_disparus && rep.services_disparus.length)
|
||||
appendTermHTML(' <span class="t-ko">services disparus :</span> ' + escapeHTML(rep.services_disparus.join(', ')) + '\n');
|
||||
if (rep.services_apparus && rep.services_apparus.length)
|
||||
appendTermHTML(' <span class="t-warn">services apparus :</span> ' + escapeHTML(rep.services_apparus.join(', ')) + '\n');
|
||||
if (rep.ports_disparus && rep.ports_disparus.length)
|
||||
appendTermHTML(' <span class="t-ko">ports disparus :</span> ' + escapeHTML(rep.ports_disparus.join(', ')) + '\n');
|
||||
if (rep.ports_apparus && rep.ports_apparus.length)
|
||||
appendTermHTML(' <span class="t-warn">ports apparus :</span> ' + escapeHTML(rep.ports_apparus.join(', ')) + '\n');
|
||||
} catch(e) {
|
||||
koCount++;
|
||||
cell.innerHTML = '<span class="text-cyber-red">✗ erreur</span>';
|
||||
termLine('✗', 'exception : ' + e.message);
|
||||
}
|
||||
}
|
||||
summary.innerHTML += ' · Post-cmp : ✓ ' + okCount + ' · ⚠ ' + warnCount + ' · ✗ ' + koCount;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user