{% extends 'base.html' %}
{% block title %}Pré-patching — iexec{% endblock %}
{% block content %}
<div class="flex justify-between items-center mb-4">
    <div>
        <h2 class="text-xl font-bold text-cyber-accent">Pré-patching — workflow iexec</h2>
        <p class="text-xs text-gray-500 mt-1">
            {{ rows|length }} serveur(s) éligible(s).
            <span class="text-cyber-yellow">Note : seuls les Linux sont concernés (Windows non géré).</span>
        </p>
    </div>
    <a href="javascript:history.back()" class="btn-sm bg-cyber-border text-cyber-accent px-4 py-2">← Retour</a>
</div>

{# ─── Stepper ─── #}
<style>
  .step-pill { padding:.35rem .75rem; border-radius:.5rem; font-weight:bold;
               font-size:.7rem; transition:all .2s; border:1px solid transparent; }
  .step-pill.s-pending { background:rgba(75,85,99,.15); color:#6b7280;
                         border-color:rgba(75,85,99,.3); opacity:.6; }
  .step-pill.s-current { background:rgba(245,158,11,.18); color:#f59e0b;
                         border-color:#f59e0b; box-shadow:0 0 12px rgba(245,158,11,.35); }
  .step-pill.s-done    { background:rgba(34,197,94,.18); color:#22c55e;
                         border-color:rgba(34,197,94,.5); }
  .step-pill.s-failed  { background:rgba(239,68,68,.2); color:#ef4444;
                         border-color:#ef4444; box-shadow:0 0 10px rgba(239,68,68,.35); }
  .step-pill.s-running { background:rgba(245,158,11,.25); color:#f59e0b;
                         border-color:#f59e0b; animation: pulseIt 1.2s infinite; }
  @keyframes pulseIt { 0%,100% { opacity:1 } 50% { opacity:.55 } }
</style>
<div class="flex items-center mb-4 gap-1 text-xs flex-wrap" id="stepper">
    <span data-step="check" class="step-pill s-current">1 Vérif</span>
    <span class="text-gray-600">→</span>
    <span data-step="snap" class="step-pill s-pending">2 Snapshot</span>
    <span class="text-gray-600">→</span>
    <span data-step="dry" class="step-pill s-pending">3a Dry-run</span>
    <span class="text-gray-600">→</span>
    <span data-step="pre" class="step-pill s-pending">3b Pre-capt</span>
    <span class="text-gray-600">→</span>
    <span data-step="patch" class="step-pill s-pending">3c Patch</span>
    <span class="text-gray-600">→</span>
    <span data-step="reboot" class="step-pill s-pending">3e Reboot</span>
    <span class="text-gray-600">→</span>
    <span data-step="recon" class="step-pill s-pending">3f Reconn.</span>
    <span class="text-gray-600">→</span>
    <span data-step="post" class="step-pill s-pending">3g Post-cmp</span>
</div>

<div class="card p-3 mb-4">
    <div class="flex items-center justify-between mb-2">
        <h3 class="text-sm font-bold text-cyber-accent">Step 1 — Vérifications pré-patching</h3>
        <div class="flex gap-2">
            <button id="btn-run-all" class="step-btn s-current" title="Lance les vérifs DNS+SSH+Disque+Satellite">1 Lancer les vérifs</button>
        </div>
    </div>
    <p class="text-xs text-gray-500 mb-3">
        Pour chaque serveur Linux : résolution DNS · connexion SSH ·
        joignabilité Satellite (LAN <code>vpdsiasat2</code> / DMZ <code>vpdsiasat1</code>) +
        <code>subscription-manager identity</code> + <code>yum repolist enabled</code>.
        Toutes les commandes utilisent <code>sudo -n</code>.
    </p>

    <table class="w-full text-xs">
        <thead class="text-cyber-accent border-b border-cyber-border">
            <tr>
                <th class="text-left p-1">Asset</th>
                <th class="text-left p-1">Hostname BDD</th>
                <th class="text-left p-1">Env</th>
                <th class="text-left p-1">Domaine</th>
                <th class="text-left p-1">OS</th>
                <th class="text-left p-1">Excludes</th>
                <th class="text-left p-1">DNS</th>
                <th class="text-left p-1">SSH</th>
                <th class="text-left p-1">Disque</th>
                <th class="text-left p-1">Satellite</th>
                <th class="text-left p-1">Verdict</th>
                <th class="text-left p-1">Snapshot</th>
                <th class="text-left p-1">Dry-run</th>
                <th class="text-left p-1">Pre-capt.</th>
                <th class="text-left p-1">Patch</th>
                <th class="text-left p-1">Reconnex.</th>
                <th class="text-left p-1">Post-cmp.</th>
            </tr>
        </thead>
        <tbody id="check-tbody">
            {% for r in rows %}
            <tr class="border-b border-cyber-border/30" data-row-id="{{ r.id }}" data-os="{{ r.os or '' }}">
                <td class="p-1 font-mono">{{ r.asset_name }}</td>
                <td class="p-1 font-mono">{{ r.hostname or '–' }}</td>
                <td class="p-1">{{ r.environnement or '' }}</td>
                <td class="p-1">{{ r.domaine if r.domaine is defined else '' }}</td>
                <td class="p-1">{{ r.os or '' }}</td>
                <td class="p-1 text-cyber-yellow">{{ r.effective_excludes or '(aucun)' }}</td>
                <td class="p-1 cell-dns text-gray-500">·</td>
                <td class="p-1 cell-ssh text-gray-500">·</td>
                <td class="p-1 cell-disk text-gray-500">·</td>
                <td class="p-1 cell-sat text-gray-500">·</td>
                <td class="p-1 cell-overall text-gray-500">en attente</td>
                <td class="p-1 cell-snap text-gray-500">·</td>
                <td class="p-1 cell-dry text-gray-500">·</td>
                <td class="p-1 cell-pre text-gray-500">·</td>
                <td class="p-1 cell-patch text-gray-500">·</td>
                <td class="p-1 cell-recon text-gray-500">·</td>
                <td class="p-1 cell-post text-gray-500">·</td>
            </tr>
            {% else %}
            <tr><td colspan="17" class="p-2 text-gray-500">Aucune ligne éligible.</td></tr>
            {% endfor %}
        </tbody>
    </table>
</div>

{# ─── Détails par serveur (panneau pliable) ─── #}
<div class="card mb-4" id="details-card" style="display:none;">
    <div class="flex justify-between items-center p-3 border-b border-cyber-border cursor-pointer"
         onclick="toggleDetails()">
        <h3 class="text-sm font-bold text-cyber-accent">
            <span id="details-arrow">▾</span> Détails du dernier check
        </h3>
        <button onclick="event.stopPropagation(); document.getElementById('details-card').style.display='none';"
                class="text-xs text-gray-500 hover:text-cyber-accent">✕ Fermer</button>
    </div>
    <pre id="details-pane" class="bg-cyber-bg p-2 text-[11px] whitespace-pre-wrap overflow-x-auto"
         style="max-height:400px;"></pre>
</div>
<script>
function toggleDetails(){
    const pane = document.getElementById('details-pane');
    const arr = document.getElementById('details-arrow');
    const collapsed = pane.style.display === 'none';
    pane.style.display = collapsed ? '' : 'none';
    arr.textContent = collapsed ? '▾' : '▸';
}
</script>

{# ─── 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">$ 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: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>

<style>
  .step-btn { padding:.5rem 1rem; border-radius:.5rem; font-weight:bold;
              font-size:.75rem; transition:all .15s; border:1px solid transparent;
              white-space:nowrap; cursor:pointer; }
  .step-btn:disabled { cursor:not-allowed; }
  .step-btn.s-pending { background:rgba(75,85,99,.15); color:#6b7280;
                        border-color:rgba(75,85,99,.3); opacity:.55; }
  .step-btn.s-current { background:rgba(245,158,11,.2); color:#f59e0b;
                        border-color:#f59e0b; box-shadow:0 0 14px rgba(245,158,11,.4); }
  .step-btn.s-current:hover { background:rgba(245,158,11,.35); }
  .step-btn.s-done    { background:rgba(34,197,94,.2); color:#22c55e;
                        border-color:rgba(34,197,94,.5); }
  .step-btn.s-done:hover { background:rgba(34,197,94,.35); }
  .step-btn.s-failed  { background:rgba(239,68,68,.2); color:#ef4444;
                        border-color:#ef4444; box-shadow:0 0 12px rgba(239,68,68,.4); }
  .step-btn.s-failed:hover { background:rgba(239,68,68,.35); }
  .step-btn.s-running { background:rgba(245,158,11,.3); color:#f59e0b;
                        border-color:#f59e0b; animation: pulseIt 1.2s infinite; }
</style>
<div class="flex justify-between items-center mt-4 flex-wrap gap-2">
    <span id="run-summary" class="text-xs text-gray-400"></span>
    <div class="flex gap-2 flex-wrap">
        <button id="btn-step2"  class="step-btn s-pending" disabled title="Snapshot vCenter">2 Snapshot</button>
        <button id="btn-dryrun" class="step-btn s-pending" disabled title="yum update --assumeno : simule sans appliquer">3a Dry-run</button>
        <button id="btn-pre"    class="step-btn s-pending" disabled title="Capture services + ports avant patch">3b Pre-capt.</button>
        <button id="btn-step3"  class="step-btn s-pending" disabled title="yum update -y : applique réellement (double confirmation)">3c Patcher</button>
        <button id="btn-reboot" class="step-btn s-pending" disabled title="shutdown -r +1 sur les serveurs patchés (double confirmation)">3e Reboot</button>
        <button id="btn-recon"  class="step-btn s-pending" disabled title="Polle TCP/22 + SSH jusqu'à reconnexion">3f Wait reconn.</button>
        <button id="btn-post"   class="step-btn s-pending" disabled title="Compare services/ports avant/après patch">3g Post-cmp.</button>
    </div>
</div>

<script>
(function(){
    const btnRun = document.getElementById('btn-run-all');
    const btnStep2 = document.getElementById('btn-step2');
    const btnDryrun = document.getElementById('btn-dryrun');
    const btnPre = document.getElementById('btn-pre');
    const btnStep3 = document.getElementById('btn-step3');
    const btnReboot = document.getElementById('btn-reboot');
    const btnRecon = document.getElementById('btn-recon');
    const btnPost = document.getElementById('btn-post');
    const tbody = document.getElementById('check-tbody');
    const summary = document.getElementById('run-summary');
    const detailsCard = document.getElementById('details-card');
    const detailsPane = document.getElementById('details-pane');

    function escapeHTML(s){
        if (s === null || s === undefined) return '';
        return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
    }

    function statusBadge(st){
        if (st === 'ok')  return '<span class="text-cyber-green">✓ OK</span>';
        if (st === 'warn')return '<span class="text-cyber-yellow">⚠ WARN</span>';
        if (st === 'ko')  return '<span class="text-cyber-red">✗ KO</span>';
        if (st === 'unsupported') return '<span class="text-gray-400">⊘ N/A</span>';
        return '<span class="text-gray-500">' + st + '</span>';
    }

    function isLinux(osStr){
        const s = (osStr || '').toLowerCase();
        return s && !s.includes('windows') && s !== 'win';
    }

    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') {
                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="' + (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;
                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'};
        }
    }

    btnRun.addEventListener('click', async () => {
        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;
        for (const tr of trs) {
            const r = await checkOne(tr);
            if (r.overall === 'ok') okCount++;
            else if (r.overall === 'warn') warnCount++;
            else if (r.overall === 'unsupported') naCount++;
            else koCount++;
        }
        summary.innerHTML = '✓ ' + okCount + ' OK · ⚠ ' + warnCount + ' warn · ✗ ' + koCount + ' KO · ⊘ ' + naCount + ' N/A';
        refreshStepButtons();
    });

    // ─── 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';
    });
    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){
        // 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
                        + (extraExcludes ? ' [retry +' + extraExcludes + ']' : '');
            termSection(titre);
            let url = '/patching/iexec/yum-stream/' + rowId + '?mode=' + mode;
            if (extraExcludes) url += '&extra_excludes=' + encodeURIComponent(extraExcludes);
            const ev = new EventSource(url);
            const result = {ok: false, rc: null, lines: 0, summary: [], problems: []};
            ev.onmessage = (m) => {
                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');
                } else if (j.type === 'line') {
                    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')
                        || ll.includes('nothing to do') || ll.includes('rien à faire')
                        || ll.includes('complete!') || ll.includes('terminé !')) {
                        result.summary.push(j.data);
                    }
                } else if (j.type === 'end') {
                    result.rc = j.rc;
                    result.problems = j.problems || [];
                    if (mode === 'dryrun') result.ok = (j.rc === 0 || j.rc === 1);
                    else result.ok = (j.rc === 0);
                    appendTerm('\n[exit code: ' + j.rc + ' — ' + (result.ok ? 'OK' : 'KO') + ']\n');
                    if (!result.ok && result.problems.length) {
                        appendTerm('[deps détectées : ' + result.problems.join(', ') + ']\n');
                    }
                    ev.close();
                    resolve(result);
                } else if (j.type === 'error') {
                    appendTerm('\n[ERROR] ' + (j.msg||'') + '\n');
                    result.error = j.msg;
                    ev.close();
                    resolve(result);
                }
            };
            ev.onerror = () => {
                appendTerm('\n[connection lost]\n');
                ev.close();
                resolve(result);
            };
        });
    }

    function buildRetryButton(rowId, mode, hostname, packages){
        // Bouton qui relance streamYum avec extra_excludes
        const btn = document.createElement('button');
        btn.className = 'text-cyber-yellow underline hover:text-cyber-accent text-[10px]';
        btn.textContent = '🔁 Retry sans ' + packages.join(', ');
        btn.title = 'Relance le yum en ajoutant ces paquets aux excludes';
        btn.addEventListener('click', async (e) => {
            e.stopPropagation();
            const extra = packages.join(' ');
            const result = await streamYum(rowId, mode, hostname, extra);
            // Met à jour la cellule
            updateYumCell(rowId, mode, hostname, result);
        });
        return btn;
    }

    function updateYumCell(rowId, mode, hostname, result){
        const tr = tbody.querySelector('tr[data-row-id="' + rowId + '"]');
        if (!tr) return;
        const cell = tr.querySelector(mode === 'dryrun' ? '.cell-dry' : '.cell-patch');
        if (mode === 'dryrun') tr._dryData = {ok: result.ok, rc: result.rc, summary: result.summary};
        else tr._patchData = {ok: result.ok, rc: result.rc, summary: result.summary};
        cell.innerHTML = '';
        if (result.ok) {
            const sumLine = (result.summary || []).slice(-2).join(' / ') || ('OK (' + result.lines + ' lignes)');
            cell.innerHTML = '<span class="text-cyber-green">✓ </span><span class="text-[10px] text-gray-300">' + escapeHTML(sumLine.slice(0,60)) + '</span>';
        } else {
            cell.innerHTML = '<span class="text-cyber-red">✗ KO (rc=' + result.rc + ')</span>';
            if ((result.problems || []).length) {
                cell.appendChild(document.createElement('br'));
                cell.appendChild(buildRetryButton(rowId, mode, hostname, result.problems));
            }
        }
        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 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 () => {
        const trs = Array.from(tbody.querySelectorAll('tr[data-row-id]'));
        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 = <intervenant>_YYYY-MM-DD_avant_patch)')) return;
        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();
            termSection('snapshot ' + host);
            const cell = tr.querySelector('.cell-snap');
            cell.innerHTML = '<span class="text-cyber-yellow">… snapshot</span>';
            try {
                const r = await fetch('/patching/iexec/snapshot/' + tr.dataset.rowId, {method:'POST'});
                const j = await r.json();
                if (j.ok) {
                    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);
            }
        }
        summary.innerHTML += ' · Snapshot : ✓ ' + okCount + ' / ✗ ' + koCount;
        refreshStepButtons();
    });

    btnDryrun.addEventListener('click', async () => {
        const trs = Array.from(tbody.querySelectorAll('tr[data-row-id]'));
        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;
        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()
                       || tr.querySelector('td:nth-child(3)').textContent.trim();
            tr.querySelector('.cell-dry').innerHTML = '<span class="text-cyber-yellow">… dry-run (live)</span>';
            const result = await streamYum(tr.dataset.rowId, 'dryrun', host);
            updateYumCell(tr.dataset.rowId, 'dryrun', host, result);
            if (result.ok) okCount++; else koCount++;
        }
        summary.innerHTML += ' · Dry-run : ✓ ' + okCount + ' / ✗ ' + koCount;
        refreshStepButtons();
    });

    btnPre.addEventListener('click', async () => {
        const trs = Array.from(tbody.querySelectorAll('tr[data-row-id]'));
        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;
        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();
            termSection('pre-capture ' + host);
            const cell = tr.querySelector('.cell-pre');
            cell.innerHTML = '<span class="text-cyber-yellow">… capture</span>';
            try {
                const r = await fetch('/patching/iexec/pre-capture/' + tr.dataset.rowId, {method:'POST'});
                const j = await r.json();
                tr._preData = j;
                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;
        refreshStepButtons();
    });

    btnReboot.addEventListener('click', async () => {
        const trs = Array.from(tbody.querySelectorAll('tr[data-row-id]'));
        const targets = trs.filter(tr => tr._patchData && tr._patchData.ok);
        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;
        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();
            termSection('reboot ' + host);
            const cell = tr.querySelector('.cell-recon');
            cell.innerHTML = '<span class="text-cyber-yellow">… reboot demandé</span>';
            try {
                const r = await fetch('/patching/iexec/reboot/' + tr.dataset.rowId, {method:'POST'});
                const j = await r.json();
                tr._rebootData = j;
                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;
        refreshStepButtons();
    });

    btnRecon.addEventListener('click', async () => {
        const trs = Array.from(tbody.querySelectorAll('tr[data-row-id]'));
        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;
        setBtnState(btnRecon, 'running'); setStepState('recon', 'running');
        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;
            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 {
                    const resp = await fetch('/patching/iexec/reboot-status/' + tr.dataset.rowId);
                    const j = await resp.json();
                    if (j.tcp22 && j.ssh) {
                        const dur = Math.round((Date.now() - t0) / 1000);
                        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;
                    }
                    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();
    });

    btnPost.addEventListener('click', async () => {
        const trs = Array.from(tbody.querySelectorAll('tr[data-row-id]'));
        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;
        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();
            termSection('post-compare ' + host);
            const cell = tr.querySelector('.cell-post');
            cell.innerHTML = '<span class="text-cyber-yellow">… compare</span>';
            try {
                const r = await fetch('/patching/iexec/post-compare/' + tr.dataset.rowId, {method:'POST'});
                const j = await r.json();
                tr._postData = j;
                const st = j.status || (j.ok ? 'ok' : 'ko');
                const rep = j.report || {};
                const dispSvc = (rep.services_disparus||[]).length;
                const appSvc = (rep.services_apparus||[]).length;
                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>'; 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;
        refreshStepButtons();
    });

    btnStep3.addEventListener('click', async () => {
        const trs = Array.from(tbody.querySelectorAll('tr[data-row-id]'));
        const targets = trs.filter(tr => tr._preData && tr._preData.ok);
        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;
        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()
                       || tr.querySelector('td:nth-child(3)').textContent.trim();
            tr.querySelector('.cell-patch').innerHTML = '<span class="text-cyber-yellow">… patch (live)</span>';
            const result = await streamYum(tr.dataset.rowId, 'update', host);
            updateYumCell(tr.dataset.rowId, 'update', host, result);
            if (result.ok) okCount++; else koCount++;
        }
        summary.innerHTML += ' · Patch : ✓ ' + okCount + ' / ✗ ' + koCount;
        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]');
        if (!tr || !tr._checkData) return;
        detailsCard.style.display = '';
        const j = tr._checkData;
        let txt = `Hostname: ${j.hostname || '–'}\nTarget: ${j.target || '–'}\nVerdict: ${j.overall}\n\n`;
        (j.checks || []).forEach(c => {
            txt += `[${c.status.toUpperCase()}] ${c.label} : ${c.message}\n`;
            if (c.details) txt += c.details + '\n';
            txt += '---\n';
        });
        detailsPane.textContent = txt;
    });
})();
</script>
{% endblock %}
