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:
Pierre & Lumière 2026-05-05 12:28:12 +02:00
parent ff95424e03
commit 0f5296ab40

View File

@ -94,13 +94,26 @@
{# ─── Terminal style log dry-run / patch ─── #} {# ─── Terminal style log dry-run / patch ─── #}
<div class="card p-2 mb-4" id="term-card" style="display:none;"> <div class="card p-2 mb-4" id="term-card" style="display:none;">
<div class="flex justify-between items-center mb-1"> <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> <h3 id="term-title" class="text-xs text-cyber-accent font-bold font-mono">$ patchcenter iexec — log</h3>
<button id="term-close" class="text-xs text-gray-500 hover:text-cyber-accent">✕ Fermer</button> <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> </div>
<pre id="term-pane" class="p-3 text-[11px] whitespace-pre-wrap overflow-auto" <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; font-family:'Cascadia Code','Consolas','Courier New',monospace;
border:1px solid #1f2937; line-height:1.4;"></pre> 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>
<div class="flex justify-between items-center mt-4 flex-wrap gap-2"> <div class="flex justify-between items-center mt-4 flex-wrap gap-2">
@ -166,20 +179,25 @@
async function checkOne(tr){ async function checkOne(tr){
const rowId = tr.dataset.rowId; const rowId = tr.dataset.rowId;
const osStr = tr.dataset.os || ''; const osStr = tr.dataset.os || '';
const host = tr.querySelector('td:nth-child(3)').textContent.trim();
if (!isLinux(osStr)) { if (!isLinux(osStr)) {
tr.querySelector('.cell-dns').innerHTML = statusBadge('unsupported'); tr.querySelector('.cell-dns').innerHTML = statusBadge('unsupported');
tr.querySelector('.cell-ssh').innerHTML = statusBadge('unsupported'); tr.querySelector('.cell-ssh').innerHTML = statusBadge('unsupported');
tr.querySelector('.cell-disk').innerHTML = statusBadge('unsupported'); tr.querySelector('.cell-disk').innerHTML = statusBadge('unsupported');
tr.querySelector('.cell-sat').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>'; 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'}; return {overall: 'unsupported'};
} }
termSection('check ' + host);
tr.querySelector('.cell-overall').innerHTML = '<span class="text-cyber-yellow"></span>'; tr.querySelector('.cell-overall').innerHTML = '<span class="text-cyber-yellow"></span>';
try { try {
const r = await fetch('/patching/iexec/check/' + rowId, {method:'POST'}); const r = await fetch('/patching/iexec/check/' + rowId, {method:'POST'});
const j = await r.json(); const j = await r.json();
if (!j.ok) { if (!j.ok) {
tr.querySelector('.cell-overall').innerHTML = '<span class="text-cyber-red">err</span>'; tr.querySelector('.cell-overall').innerHTML = '<span class="text-cyber-red">err</span>';
termLine('✗', 'erreur backend');
return {overall: 'ko'}; return {overall: 'ko'};
} }
if (j.overall === 'unsupported') { if (j.overall === 'unsupported') {
@ -188,19 +206,30 @@
tr.querySelector('.cell-disk').innerHTML = statusBadge('unsupported'); tr.querySelector('.cell-disk').innerHTML = statusBadge('unsupported');
tr.querySelector('.cell-sat').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>'; 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; return j;
} }
appendTerm(' # target=' + (j.target || '?') + '\n');
const byName = {}; 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-dns').innerHTML = byName.dns ? statusBadge(byName.dns.status) : '';
tr.querySelector('.cell-ssh').innerHTML = byName.ssh ? statusBadge(byName.ssh.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-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-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>'; 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; tr._checkData = j;
return j; return j;
} catch(e) { } catch(e) {
tr.querySelector('.cell-overall').innerHTML = '<span class="text-cyber-red">err</span>'; tr.querySelector('.cell-overall').innerHTML = '<span class="text-cyber-red">err</span>';
termLine('✗', 'exception : ' + e.message);
return {overall: 'ko'}; return {overall: 'ko'};
} }
} }
@ -222,29 +251,65 @@
btnStep2.disabled = (okCount === 0); btnStep2.disabled = (okCount === 0);
}); });
// ─── Terminal ─── // ─── Terminal global (toutes les étapes) ───
const termCard = document.getElementById('term-card'); const termCard = document.getElementById('term-card');
const termTitle = document.getElementById('term-title'); const termTitle = document.getElementById('term-title');
const termPane = document.getElementById('term-pane'); const termPane = document.getElementById('term-pane');
document.getElementById('term-close').addEventListener('click', () => { document.getElementById('term-close').addEventListener('click', () => {
termCard.style.display = 'none'; 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 = ''; termCard.style.display = '';
termTitle.textContent = '$ ' + title;
termPane.textContent = '';
termCard.scrollIntoView({behavior:'smooth', block:'nearest'}); termCard.scrollIntoView({behavior:'smooth', block:'nearest'});
} }
function appendTerm(s){ }
termPane.textContent += s; function appendTermHTML(html){
termPane.insertAdjacentHTML('beforeend', html);
termPane.scrollTop = termPane.scrollHeight; 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){ function streamYum(rowId, mode, hostname, extraExcludes){
return new Promise((resolve) => { 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 + ']' : ''); + (extraExcludes ? ' [retry +' + extraExcludes + ']' : '');
openTerm(titre); termSection(titre);
let url = '/patching/iexec/yum-stream/' + rowId + '?mode=' + mode; let url = '/patching/iexec/yum-stream/' + rowId + '?mode=' + mode;
if (extraExcludes) url += '&extra_excludes=' + encodeURIComponent(extraExcludes); if (extraExcludes) url += '&extra_excludes=' + encodeURIComponent(extraExcludes);
const ev = new EventSource(url); const ev = new EventSource(url);
@ -253,11 +318,13 @@
let j; let j;
try { j = JSON.parse(m.data); } catch(e) { return; } try { j = JSON.parse(m.data); } catch(e) { return; }
if (j.type === 'cmd') { if (j.type === 'cmd') {
appendTerm('# host : ' + (j.hostname||'') + ' (' + (j.target||'') + ')\n'); appendTerm(' # host : ' + (j.hostname||'') + ' (' + (j.target||'') + ')\n');
appendTerm('# cmd : ' + (j.cmd||'') + '\n'); appendTerm(' # cmd : ' + (j.cmd||'') + '\n');
appendTerm('# excludes (' + (j.excludes||[]).length + ')\n\n'); appendTerm(' # excludes (' + (j.excludes||[]).length + ')\n');
} else if (j.type === 'line') { } 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++; result.lines++;
const ll = j.data.toLowerCase(); const ll = j.data.toLowerCase();
if (ll.includes('package') || ll.includes('paquet') if (ll.includes('package') || ll.includes('paquet')
@ -353,6 +420,8 @@
btnRun.disabled = true; btnRun.disabled = true;
let okCount = 0, koCount = 0; let okCount = 0, koCount = 0;
for (const tr of okTrs) { for (const tr of okTrs) {
const host = tr.querySelector('td:nth-child(3)').textContent.trim();
termSection('snapshot ' + host);
const cell = tr.querySelector('.cell-snap'); const cell = tr.querySelector('.cell-snap');
cell.innerHTML = '<span class="text-cyber-yellow">… snapshot</span>'; cell.innerHTML = '<span class="text-cyber-yellow">… snapshot</span>';
try { try {
@ -362,17 +431,22 @@
okCount++; okCount++;
cell.innerHTML = '<span class="text-cyber-green" title="' + escapeHTML(j.detail||'') + '">✓ ' + escapeHTML(j.snap_name||'OK') + '</span>' 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>'; + ' <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) { } else if (j.skipped) {
koCount++; koCount++;
cell.innerHTML = '<span class="text-cyber-yellow">⚠ ' + escapeHTML(j.detail||'skip') + '</span>'; cell.innerHTML = '<span class="text-cyber-yellow">⚠ ' + escapeHTML(j.detail||'skip') + '</span>';
termLine('⚠', j.detail || 'skipped');
} else { } else {
koCount++; koCount++;
cell.innerHTML = '<span class="text-cyber-red" title="' + escapeHTML(j.detail||'') + '">✗ ' + escapeHTML((j.detail||'KO').slice(0,80)) + '</span>'; 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; tr._snapData = j;
} catch(e) { } catch(e) {
koCount++; koCount++;
cell.innerHTML = '<span class="text-cyber-red">✗ erreur réseau</span>'; cell.innerHTML = '<span class="text-cyber-red">✗ erreur réseau</span>';
termLine('✗', 'exception : ' + e.message);
} }
} }
btnRun.disabled = false; btnRun.disabled = false;
@ -407,6 +481,8 @@
btnPre.disabled = true; btnStep3.disabled = true; btnPre.disabled = true; btnStep3.disabled = true;
let okCount = 0, koCount = 0; let okCount = 0, koCount = 0;
for (const tr of targets) { 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'); const cell = tr.querySelector('.cell-pre');
cell.innerHTML = '<span class="text-cyber-yellow">… capture</span>'; cell.innerHTML = '<span class="text-cyber-yellow">… capture</span>';
try { try {
@ -416,13 +492,18 @@
if (j.ok) { if (j.ok) {
okCount++; okCount++;
cell.innerHTML = '<span class="text-cyber-green" title="' + escapeHTML(j.stdout||'') + '">✓ snapshot</span>'; 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 { } else {
koCount++; koCount++;
cell.innerHTML = '<span class="text-cyber-red" title="' + escapeHTML(j.detail||j.stderr||'') + '">✗ ' + escapeHTML((j.detail||'KO').slice(0,80)) + '</span>'; 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) { } catch(e) {
koCount++; koCount++;
cell.innerHTML = '<span class="text-cyber-red">✗ erreur</span>'; cell.innerHTML = '<span class="text-cyber-red">✗ erreur</span>';
termLine('✗', 'exception : ' + e.message);
} }
} }
summary.innerHTML += ' · Pre-capt : ✓ ' + okCount + ' / ✗ ' + koCount; summary.innerHTML += ' · Pre-capt : ✓ ' + okCount + ' / ✗ ' + koCount;
@ -438,6 +519,8 @@
btnReboot.disabled = true; btnReboot.disabled = true;
let okCount = 0, koCount = 0; let okCount = 0, koCount = 0;
for (const tr of targets) { for (const tr of targets) {
const host = tr.querySelector('td:nth-child(3)').textContent.trim();
termSection('reboot ' + host);
const cell = tr.querySelector('.cell-recon'); const cell = tr.querySelector('.cell-recon');
cell.innerHTML = '<span class="text-cyber-yellow">… reboot demandé</span>'; cell.innerHTML = '<span class="text-cyber-yellow">… reboot demandé</span>';
try { try {
@ -447,13 +530,17 @@
if (j.ok) { if (j.ok) {
okCount++; okCount++;
cell.innerHTML = '<span class="text-cyber-yellow">⏳ reboot dans 1min · ' + escapeHTML(j.started_at||'') + '</span>'; 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 { } else {
koCount++; koCount++;
cell.innerHTML = '<span class="text-cyber-red" title="' + escapeHTML(j.detail||j.stderr||'') + '">✗ ' + escapeHTML((j.detail||'KO').slice(0,80)) + '</span>'; 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) { } catch(e) {
koCount++; koCount++;
cell.innerHTML = '<span class="text-cyber-red">✗ erreur</span>'; cell.innerHTML = '<span class="text-cyber-red">✗ erreur</span>';
termLine('✗', 'exception : ' + e.message);
} }
} }
summary.innerHTML += ' · Reboot : ✓ ' + okCount + ' / ✗ ' + koCount; summary.innerHTML += ' · Reboot : ✓ ' + okCount + ' / ✗ ' + koCount;
@ -468,11 +555,14 @@
const startTs = Date.now(); const startTs = Date.now();
// Pour chaque target, polling indépendant // Pour chaque target, polling indépendant
await Promise.all(targets.map(async (tr) => { 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 cell = tr.querySelector('.cell-recon');
const t0 = Date.now(); const t0 = Date.now();
const TIMEOUT_MS = 10 * 60 * 1000; // 10 min const TIMEOUT_MS = 10 * 60 * 1000;
const POLL_MS = 10 * 1000; const POLL_MS = 10 * 1000;
cell.innerHTML = '<span class="text-cyber-yellow">⏳ poll TCP/22…</span>'; cell.innerHTML = '<span class="text-cyber-yellow">⏳ poll TCP/22…</span>';
let last = '';
while (Date.now() - t0 < TIMEOUT_MS) { while (Date.now() - t0 < TIMEOUT_MS) {
await new Promise(r => setTimeout(r, POLL_MS)); await new Promise(r => setTimeout(r, POLL_MS));
try { try {
@ -483,15 +573,21 @@
tr._reconData = {ok: true, downtime_s: dur, uptime: j.uptime}; tr._reconData = {ok: true, downtime_s: dur, uptime: j.uptime};
cell.innerHTML = '<span class="text-cyber-green">✓ revenu en ' + dur + 's</span>' 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>'; + '<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; return;
} }
cell.innerHTML = '<span class="text-cyber-yellow">⏳ ' const status = j.tcp22 ? 'TCP/22 OK · SSH KO' : 'pas joignable';
+ (j.tcp22 ? 'TCP/22 OK · SSH KO' : 'pas joignable') const elapsed = Math.round((Date.now()-t0)/1000);
+ ' · ' + Math.round((Date.now()-t0)/1000) + 's</span>'; cell.innerHTML = '<span class="text-cyber-yellow">⏳ ' + status + ' · ' + elapsed + 's</span>';
} catch(e) { /* ignore, retry */ } if (status !== last) {
termLine('⏳', host + ' : ' + status + ' (t+' + elapsed + 's)');
last = status;
}
} catch(e) { /* retry */ }
} }
tr._reconData = {ok: false}; tr._reconData = {ok: false};
cell.innerHTML = '<span class="text-cyber-red">✗ timeout 10 min</span>'; cell.innerHTML = '<span class="text-cyber-red">✗ timeout 10 min</span>';
termLine('✗', host + ' : timeout 10 min sans reconnexion');
})); }));
refreshStepButtons(); refreshStepButtons();
}); });
@ -504,6 +600,8 @@
btnPost.disabled = true; btnPost.disabled = true;
let okCount = 0, warnCount = 0, koCount = 0; let okCount = 0, warnCount = 0, koCount = 0;
for (const tr of targets) { 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'); const cell = tr.querySelector('.cell-post');
cell.innerHTML = '<span class="text-cyber-yellow">… compare</span>'; cell.innerHTML = '<span class="text-cyber-yellow">… compare</span>';
try { try {
@ -517,12 +615,21 @@
const dispPort = (rep.ports_disparus||[]).length; const dispPort = (rep.ports_disparus||[]).length;
const appPort = (rep.ports_apparus||[]).length; const appPort = (rep.ports_apparus||[]).length;
const summ = 'svc -' + dispSvc + ' +' + appSvc + ' / port -' + dispPort + ' +' + appPort; const summ = 'svc -' + dispSvc + ' +' + appSvc + ' / port -' + dispPort + ' +' + appPort;
if (st === 'ok') { okCount++; cell.innerHTML = '<span class="text-cyber-green">✓ ' + 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>'; } 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>'; } 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) { } catch(e) {
koCount++; koCount++;
cell.innerHTML = '<span class="text-cyber-red">✗ erreur</span>'; cell.innerHTML = '<span class="text-cyber-red">✗ erreur</span>';
termLine('✗', 'exception : ' + e.message);
} }
} }
summary.innerHTML += ' · Post-cmp : ✓ ' + okCount + ' · ⚠ ' + warnCount + ' · ✗ ' + koCount; summary.innerHTML += ' · Post-cmp : ✓ ' + okCount + ' · ⚠ ' + warnCount + ' · ✗ ' + koCount;