patchcenter/app/templates/safe_patching_detail.html
Khalid MOUTAOUAKIL 833c4fc3d2 Quick Win delete, UI planning/specifics reorganises, accents retires
- Safe Patching: bouton supprimer campagne (admin only)
- Safe Patching: boutons nouvelle campagne et planning en haut
- Safe Patching: message suppression dans les notifications
- Planning: formulaire ajouter deplace apres le Gantt (compact)
- Planning: accents retires des messages flash
- Specifics: formulaire ajouter deplace en haut avant le tableau
- Specifics: colonne Wave retiree, colonnes Stop/Start renommees

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:44:22 +02:00

216 lines
13 KiB
HTML

{% extends 'base.html' %}
{% block title %}{{ c.label }}{% endblock %}
{% block content %}
<div class="flex justify-between items-center mb-4">
<div>
<a href="/safe-patching" class="text-xs text-gray-500 hover:text-gray-300">← Safe Patching</a>
<h2 class="text-xl font-bold text-cyber-accent">{{ c.label }}</h2>
<div class="flex items-center gap-3 mt-1">
<span class="badge badge-yellow">quickwin</span>
<span class="badge {% if c.status == 'draft' %}badge-gray{% elif c.status == 'in_progress' %}badge-yellow{% elif c.status == 'completed' %}badge-green{% else %}badge-red{% endif %}">{{ c.status }}</span>
</div>
</div>
{% set p = perms if perms is defined else request.state.perms %}
{% if p.campaigns == 'admin' %}
<form method="POST" action="/safe-patching/{{ c.id }}/delete">
<button class="btn-sm bg-red-900/50 text-cyber-red px-4 py-2" onclick="return confirm('SUPPRIMER définitivement cette campagne Quick Win ?')">Supprimer</button>
</form>
{% endif %}
</div>
{% if msg %}
<div class="mb-3 p-2 rounded text-sm {% if 'error' in msg or 'no_pending' in msg %}bg-red-900/30 text-cyber-red{% else %}bg-green-900/30 text-cyber-green{% endif %}">
{% if msg.startswith('excluded_') %}{{ msg.split('_')[1] }} serveur(s) exclu(s).{% elif msg == 'no_pending' %}Aucun serveur en attente.{% elif msg == 'prereqs_done' %}Prérequis vérifiés.{% endif %}
</div>
{% endif %}
<!-- KPIs par branche -->
<div class="grid grid-cols-2 gap-4 mb-4">
<div class="card p-4">
<h3 class="text-sm font-bold text-cyber-yellow mb-2">Branche 1 — Hors-prod</h3>
<div class="flex gap-3 text-sm">
<span class="text-cyber-accent">{{ qw_stats.hprod_total }} total</span>
<span class="text-cyber-green">{{ qw_stats.hprod_patched }} patchés</span>
<span class="text-cyber-red">{{ qw_stats.hprod_failed }} échoués</span>
</div>
</div>
<div class="card p-4">
<h3 class="text-sm font-bold text-cyber-green mb-2">Branche 2 — Production</h3>
<div class="flex gap-3 text-sm">
<span class="text-cyber-accent">{{ qw_stats.prod_total }} total</span>
<span class="text-cyber-green">{{ qw_stats.prod_patched }} patchés</span>
<span class="text-cyber-red">{{ qw_stats.prod_failed }} échoués</span>
</div>
</div>
</div>
<!-- Steps wizard -->
<div x-data="{ step: '{{ current_step }}' }" class="space-y-3">
<!-- Step nav -->
<div class="flex gap-1 mb-4">
{% for s in ['prereqs','snapshot','execute','postcheck'] %}
<button @click="step = '{{ s }}'" class="px-3 py-1 text-xs rounded"
:class="step === '{{ s }}' ? 'bg-cyber-accent text-black font-bold' : 'bg-cyber-border text-gray-400'">
{{ loop.index }}. {% if s == 'prereqs' %}Prérequis{% elif s == 'snapshot' %}Snapshot{% elif s == 'execute' %}Exécution{% elif s == 'postcheck' %}Post-patch{% endif %}
</button>
{% endfor %}
</div>
<!-- Step 1: Prérequis -->
<div x-show="step === 'prereqs'" class="card overflow-x-auto">
<div class="p-3 border-b border-cyber-border flex justify-between items-center">
<h3 class="text-sm font-bold text-cyber-accent">Step 1 — Vérification prérequis</h3>
<div class="flex gap-2">
<form method="POST" action="/safe-patching/{{ c.id }}/check-prereqs" style="display:inline">
<input type="hidden" name="branch" value="hprod">
<button class="btn-primary px-3 py-1 text-sm" data-loading="Vérification prérequis...|Connexion SSH à chaque serveur">Vérifier hors-prod</button>
</form>
<form method="POST" action="/safe-patching/{{ c.id }}/check-prereqs" style="display:inline">
<input type="hidden" name="branch" value="prod">
<button class="btn-sm bg-cyber-border text-cyber-accent" data-loading="Vérification prérequis...|Connexion SSH à chaque serveur">Vérifier prod</button>
</form>
</div>
</div>
<div id="excl-bar-prereq" class="p-2 border-b border-cyber-border flex gap-2 items-center" style="display:none">
<span class="text-xs text-gray-400" id="excl-count-prereq">0</span>
<form method="POST" action="/safe-patching/{{ c.id }}/bulk-exclude">
<input type="hidden" name="session_ids" id="excl-ids-prereq">
<button class="btn-sm bg-red-900/30 text-cyber-red" onclick="document.getElementById('excl-ids-prereq').value=getCheckedPrereq()">Exclure sélection</button>
</form>
</div>
<table class="w-full table-cyber text-xs">
<thead><tr>
<th class="p-2 w-6"><input type="checkbox" onchange="document.querySelectorAll('.chk-prereq').forEach(function(c){c.checked=this.checked}.bind(this)); updateExclPrereq()"></th>
<th class="text-left p-2">Hostname</th>
<th class="p-2">Env</th>
<th class="p-2">Domaine</th>
<th class="p-2">SSH</th>
<th class="p-2">Disque</th>
<th class="p-2">Satellite</th>
<th class="p-2">État</th>
</tr></thead>
<tbody>
{% for s in sessions %}
{% if s.status != 'excluded' %}
<tr class="{% if s.prereq_validated == false and s.prereq_date %}bg-red-900/10{% endif %}">
<td class="p-2 text-center">{% if s.status == 'pending' %}<input type="checkbox" class="chk-prereq" value="{{ s.id }}" onchange="updateExclPrereq()">{% endif %}</td>
<td class="p-2 font-mono text-cyber-accent">{{ s.hostname }}</td>
<td class="p-2 text-center"><span class="badge {% if s.environnement == 'Production' %}badge-green{% else %}badge-yellow{% endif %}">{{ (s.environnement or '')[:6] }}</span></td>
<td class="p-2 text-center text-xs">{{ s.domaine or '-' }}</td>
<td class="p-2 text-center">{% if s.prereq_ssh == 'ok' %}<span class="text-cyber-green">OK</span>{% elif s.prereq_ssh == 'ko' %}<span class="text-cyber-red">KO</span>{% else %}<span class="text-gray-600"></span>{% endif %}</td>
<td class="p-2 text-center">{% if s.prereq_disk_ok is true %}<span class="text-cyber-green">OK</span>{% elif s.prereq_disk_ok is false %}<span class="text-cyber-red">KO</span>{% else %}<span class="text-gray-600"></span>{% endif %}</td>
<td class="p-2 text-center">{% if s.prereq_satellite == 'ok' %}<span class="text-cyber-green">OK</span>{% elif s.prereq_satellite == 'ko' %}<span class="text-cyber-red">KO</span>{% elif s.prereq_satellite == 'na' %}<span class="text-gray-500">N/A</span>{% else %}<span class="text-gray-600"></span>{% endif %}</td>
<td class="p-2 text-center">{% if s.prereq_validated %}<span class="badge badge-green">OK</span>{% elif s.prereq_date %}<span class="badge badge-red">KO</span>{% else %}<span class="text-gray-600"></span>{% endif %}</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
<!-- Step 2: Snapshot -->
<div x-show="step === 'snapshot'" class="card p-4">
<h3 class="text-sm font-bold text-cyber-accent mb-3">Step 2 — Snapshot vSphere</h3>
<p class="text-xs text-gray-500 mb-3">Créer un snapshot sur toutes les VMs avant patching. Les serveurs physiques sont ignorés.</p>
<form method="POST" action="/safe-patching/{{ c.id }}/snapshot">
<input type="hidden" name="branch" value="hprod">
<button class="btn-primary px-4 py-2 text-sm" data-loading="Création snapshots...|Connexion vSphere en cours">Créer snapshots hors-prod</button>
</form>
</div>
<!-- Step 3: Exécution -->
<div x-show="step === 'execute'" class="card p-4">
<h3 class="text-sm font-bold text-cyber-accent mb-3">Step 3 — Exécution Safe Patching</h3>
<div class="mb-4">
<h4 class="text-xs text-gray-500 mb-1">Commande yum (éditable)</h4>
<textarea id="yum-cmd" rows="3" class="w-full font-mono text-xs">{{ safe_cmd }}</textarea>
<p class="text-xs text-gray-600 mt-1">{{ safe_excludes|length }} packages exclus. Modifiez si besoin avant de lancer.</p>
</div>
<div class="flex gap-3">
<form method="POST" action="/safe-patching/{{ c.id }}/execute">
<input type="hidden" name="branch" value="hprod">
<button class="btn-primary px-4 py-2 text-sm" data-loading="Lancement hors-prod...|Sauvegarde état + patching">Lancer hors-prod</button>
</form>
{% if qw_stats.hprod_total > 0 and qw_stats.hprod_patched == qw_stats.hprod_total %}
<form method="POST" action="/safe-patching/{{ c.id }}/execute">
<input type="hidden" name="branch" value="prod">
<button class="btn-sm bg-cyber-green text-black px-4 py-2" data-loading="Lancement production...|Sauvegarde état + patching" onclick="return confirm('Lancer le patching PRODUCTION ?')">Lancer production</button>
</form>
{% else %}
<span class="text-xs text-gray-500 py-2">Production disponible après hors-prod à 100%</span>
{% endif %}
</div>
</div>
<!-- Step 4: Post-patching -->
<div x-show="step === 'postcheck'" class="card p-4">
<h3 class="text-sm font-bold text-cyber-accent mb-3">Step 4 — Vérification post-patch</h3>
<p class="text-xs text-gray-500 mb-3">Vérifier les services, ports et needs-restarting après patching.</p>
<div class="flex gap-3 mb-4">
<form method="POST" action="/safe-patching/{{ c.id }}/postcheck">
<input type="hidden" name="branch" value="hprod">
<button class="btn-primary px-3 py-1 text-sm" data-loading="Vérification post-patch...|Comparaison services avant/après">Vérifier hors-prod</button>
</form>
<form method="POST" action="/safe-patching/{{ c.id }}/postcheck">
<input type="hidden" name="branch" value="prod">
<button class="btn-sm bg-cyber-border text-cyber-accent" data-loading="Vérification post-patch...|Comparaison services avant/après">Vérifier prod</button>
</form>
<a href="/safe-patching/{{ c.id }}/export" class="btn-sm bg-cyber-green text-black">Export CSV</a>
</div>
<!-- Résultats -->
<table class="w-full table-cyber text-xs">
<thead><tr>
<th class="text-left p-2">Hostname</th>
<th class="p-2">Env</th>
<th class="p-2">Statut</th>
<th class="p-2">Packages</th>
<th class="p-2">Reboot</th>
<th class="p-2">Services</th>
</tr></thead>
<tbody>
{% for s in sessions %}
{% if s.status in ('patched', 'failed') %}
<tr class="{% if s.status == 'failed' %}bg-red-900/10{% endif %}">
<td class="p-2 font-mono text-cyber-accent">{{ s.hostname }}</td>
<td class="p-2 text-center"><span class="badge {% if s.environnement == 'Production' %}badge-green{% else %}badge-yellow{% endif %}">{{ (s.environnement or '')[:6] }}</span></td>
<td class="p-2 text-center"><span class="badge {% if s.status == 'patched' %}badge-green{% else %}badge-red{% endif %}">{{ s.status }}</span></td>
<td class="p-2 text-center text-gray-400">{{ s.packages_updated or 0 }}</td>
<td class="p-2 text-center">{% if s.reboot_required %}<span class="text-cyber-red">Oui</span>{% else %}<span class="text-cyber-green">Non</span>{% endif %}</td>
<td class="p-2 text-center">{% if s.postcheck_services == 'ok' %}<span class="text-cyber-green">OK</span>{% elif s.postcheck_services == 'ko' %}<span class="text-cyber-red">KO</span>{% else %}<span class="text-gray-600"></span>{% endif %}</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
</div>
{% if excluded %}
<details class="card mt-4">
<summary class="p-3 cursor-pointer text-sm text-gray-500">{{ excluded|length }} serveur(s) exclu(s)</summary>
<div class="p-3 text-xs text-gray-600 font-mono">
{% for s in excluded %}{{ s.hostname }}{% if not loop.last %}, {% endif %}{% endfor %}
</div>
</details>
{% endif %}
<script>
function getCheckedPrereq() {
return Array.from(document.querySelectorAll('.chk-prereq:checked')).map(function(c){return c.value}).join(',');
}
function updateExclPrereq() {
var count = document.querySelectorAll('.chk-prereq:checked').length;
var bar = document.getElementById('excl-bar-prereq');
bar.style.display = count > 0 ? 'flex' : 'none';
document.getElementById('excl-count-prereq').textContent = count + ' sélectionné(s)';
}
</script>
{% endblock %}