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 ─── #}
-
$ terminal
-
+
$ patchcenter iexec — log
+
+
+
+
+
@@ -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;