patchcenter/app/templates/quickwin_config.html
Khalid MOUTAOUAKIL 5cc10c5b6c Module QuickWin complet + filtres serveurs OS/owner
- QuickWin: campagnes patching rapide avec exclusions générales (OS/reboot) et spécifiques (applicatifs)
- Config serveurs: pagination, filtres (search, env, domain, zone, per_page), dry run, bulk edit
- Détail campagne: pagination hprod/prod séparée, filtres (search, status, domain), section prod masquée si hprod non terminé
- Auth: redirection qw_only vers /quickwin, profil lecture seule quickwin
- Serveurs: filtres OS (Linux/Windows) et Owner (secops/ipop/na), exclusion EOL
- Sidebar: lien QuickWin conditionné sur permission campaigns ou quickwin

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 16:27:45 +02:00

207 lines
12 KiB
HTML

{% extends "base.html" %}
{% block title %}QuickWin Config{% endblock %}
{% macro qs(p) -%}
?page={{ p }}&per_page={{ per_page }}&search={{ filters.search or '' }}&env={{ filters.env or '' }}&domain={{ filters.domain or '' }}&zone={{ filters.zone or '' }}
{%- endmacro %}
{% block content %}
<div class="flex items-center justify-between mb-6">
<div>
<a href="/quickwin" class="text-xs text-gray-500 hover:text-gray-300">&larr; Retour QuickWin</a>
<h1 class="text-xl font-bold" style="color:#00d4ff">Exclusions par serveur</h1>
<p class="text-xs text-gray-500">Tous les serveurs Linux en_production / secops &mdash; exclusions g&eacute;n&eacute;rales par d&eacute;faut pr&eacute;-remplies &mdash; pas de reboot n&eacute;cessaire</p>
</div>
<div class="flex gap-2 items-center">
<span class="text-sm text-gray-400">{{ total_count }} serveur(s)</span>
<button onclick="document.getElementById('bulkModal').style.display='flex'" class="btn-primary" style="padding:6px 16px;font-size:0.85rem">Modifier en masse</button>
</div>
</div>
{% if msg %}
<div style="background:#1a5a2e;color:#8f8;padding:8px 16px;border-radius:6px;margin-bottom:12px;font-size:0.85rem">
{% if 'saved' in msg %}Configuration sauvegard&eacute;e{% elif 'deleted' in msg %}Exclusions sp&eacute;cifiques retir&eacute;es{% elif 'added' in msg %}{{ msg.split('_')[1] }} serveur(s) mis &agrave; jour{% elif 'bulk' in msg %}Mise &agrave; jour group&eacute;e OK{% else %}{{ msg }}{% endif %}
</div>
{% endif %}
<!-- Filtre -->
<form method="GET" class="card mb-4" style="padding:10px 16px;display:flex;gap:12px;align-items:center">
<input type="text" name="search" value="{{ filters.search or '' }}" placeholder="Recherche serveur..." style="width:200px">
<select name="env" onchange="this.form.submit()" style="width:140px">
<option value="">Tous env.</option>
{% set envs = all_configs|map(attribute='environnement')|select('string')|unique|sort %}
{% for e in envs %}<option value="{{ e }}" {% if filters.env == e %}selected{% endif %}>{{ e }}</option>{% endfor %}
</select>
<select name="domain" onchange="this.form.submit()" style="width:140px">
<option value="">Tous domaines</option>
{% set doms = all_configs|map(attribute='domaine')|select('string')|unique|sort %}
{% for d in doms %}<option value="{{ d }}" {% if filters.domain == d %}selected{% endif %}>{{ d }}</option>{% endfor %}
</select>
<select name="zone" onchange="this.form.submit()" style="width:100px">
<option value="">Zone</option>
{% set zones = all_configs|map(attribute='zone')|select('string')|unique|sort %}
{% for z in zones %}<option value="{{ z }}" {% if filters.zone == z %}selected{% endif %}>{{ z }}</option>{% endfor %}
</select>
<select name="per_page" onchange="this.form.submit()" style="width:140px">
<option value="">Affichage / page</option>
{% for n in [14, 25, 50, 100] %}<option value="{{ n }}" {% if per_page == n %}selected{% endif %}>{{ n }} par page</option>{% endfor %}
</select>
<button type="submit" class="btn-primary" style="padding:4px 14px;font-size:0.8rem">Filtrer</button>
<a href="/quickwin/config" class="text-xs text-gray-500 hover:text-gray-300">Reset</a>
<span class="text-xs text-gray-500">{{ total_count }} serveur(s)</span>
</form>
<!-- Cartouche detail serveur -->
<div id="srvDetail" class="card mb-4" style="display:none;border-left:3px solid #00d4ff;padding:12px 16px">
<div class="flex items-center justify-between mb-2">
<h3 style="color:#00d4ff;font-weight:bold;font-size:0.95rem" id="detailName"></h3>
<button onclick="document.getElementById('srvDetail').style.display='none'" class="text-gray-500 hover:text-gray-300" style="font-size:1.2rem">&times;</button>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<div class="text-xs text-gray-500 mb-1 font-bold">Exclusions g&eacute;n&eacute;rales (OS / reboot)</div>
<pre id="detailGeneral" style="font-size:0.7rem;color:#ffcc00;white-space:pre-wrap;margin:0"></pre>
</div>
<div>
<div class="text-xs text-gray-500 mb-1 font-bold">Exclusions sp&eacute;cifiques (applicatifs &mdash; hors p&eacute;rim&egrave;tre secops)</div>
<pre id="detailSpecific" style="font-size:0.7rem;color:#ff8800;white-space:pre-wrap;margin:0"></pre>
</div>
</div>
</div>
<!-- Tableau serveurs -->
<div class="card">
<div class="table-wrap">
<table class="table-cyber w-full" id="srvTable">
<thead><tr>
<th class="px-2 py-2" style="width:30px"><input type="checkbox" id="checkAll" onchange="toggleAll(this)"></th>
<th class="px-2 py-2">Serveur</th>
<th class="px-2 py-2">Domaine</th>
<th class="px-2 py-2">Env</th>
<th class="px-2 py-2">Zone</th>
<th class="px-2 py-2">Tier</th>
<th class="px-2 py-2">Exclusions g&eacute;n&eacute;rales</th>
<th class="px-2 py-2">Exclusions sp&eacute;cifiques</th>
<th class="px-2 py-2">Notes</th>
<th class="px-2 py-2" style="width:60px">Save</th>
<th class="px-2 py-2" style="width:60px">Cmd</th>
</tr></thead>
<tbody>
{% for s in all_servers %}
<tr>
<td class="px-2 py-2"><input type="checkbox" class="srv-check" value="{{ s.server_id }}"></td>
<td class="px-2 py-2 font-bold" style="color:#00d4ff;cursor:pointer" onclick="showDetail('{{ s.hostname }}', this)">{{ s.hostname }}</td>
<td class="px-2 py-2 text-gray-400 text-xs">{{ s.domaine or '?' }}</td>
<td class="px-2 py-2 text-gray-400 text-xs">{{ s.environnement or '?' }}</td>
<td class="px-2 py-2 text-center"><span class="badge {% if s.zone == 'DMZ' %}badge-red{% else %}badge-blue{% endif %}">{{ s.zone or 'LAN' }}</span></td>
<td class="px-2 py-2 text-gray-400 text-xs">{{ s.tier }}</td>
<td class="px-2 py-2">
<form method="post" action="/quickwin/config/save" class="inline-form" style="display:flex;gap:4px;align-items:center">
<input type="hidden" name="server_id" value="{{ s.server_id }}">
<input type="text" name="general_excludes" value="{{ s.general_excludes }}"
style="width:200px;font-size:0.7rem;padding:2px 6px" title="{{ s.general_excludes }}">
</td>
<td class="px-2 py-2">
<input type="text" name="specific_excludes" value="{{ s.specific_excludes }}"
style="width:150px;font-size:0.7rem;padding:2px 6px" placeholder="sdcss* custom*...">
</td>
<td class="px-2 py-2">
<input type="text" name="notes" value="{{ s.notes }}"
style="width:80px;font-size:0.7rem;padding:2px 6px" placeholder="...">
<button type="submit" class="btn-sm" style="background:#1e3a5f;color:#00d4ff;font-size:0.65rem">OK</button>
</form>
</td>
<td class="px-2 py-2">
<button type="button" class="btn-sm" style="background:#1a3a1a;color:#00ff88;font-size:0.6rem;white-space:nowrap" onclick="showDryRun('{{ s.hostname }}', this)">Dry Run</button>
</td>
</tr>
{% endfor %}
{% if not all_servers %}<tr><td colspan="11" class="px-2 py-8 text-center text-gray-500">Aucun serveur trouv&eacute;</td></tr>{% endif %}
</tbody>
</table>
</div>
</div>
<!-- Pagination -->
<div class="flex justify-between items-center mt-4 text-sm text-gray-500">
<span>Page {{ page }} / {{ total_pages }} &mdash; {{ total_count }} serveurs</span>
<div class="flex gap-2">
{% if page > 1 %}<a href="{{ qs(page - 1) }}" class="btn-sm bg-cyber-border text-gray-300">Pr&eacute;c&eacute;dent</a>{% endif %}
{% if page < total_pages %}<a href="{{ qs(page + 1) }}" class="btn-sm bg-cyber-border text-gray-300">Suivant</a>{% endif %}
</div>
</div>
<!-- Bulk modal -->
<div id="bulkModal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.7);z-index:100;align-items:center;justify-content:center" onclick="if(event.target===this)this.style.display='none'">
<div class="card" style="width:550px;max-width:90vw;padding:24px">
<h3 style="color:#00d4ff;font-weight:bold;margin-bottom:12px">Modification group&eacute;e</h3>
<p class="text-xs text-gray-500 mb-3">Cochez les serveurs dans le tableau, puis appliquez les exclusions.</p>
<form method="post" action="/quickwin/config/bulk-add">
<input type="hidden" name="server_ids" id="bulkIds">
<div class="mb-3">
<label class="text-xs text-gray-400 block mb-1">Exclusions g&eacute;n&eacute;rales</label>
<textarea name="general_excludes" rows="3" style="width:100%;font-size:0.75rem">{{ default_excludes }}</textarea>
</div>
<div class="flex gap-2 justify-end">
<button type="button" class="btn-sm" style="background:#333;color:#ccc;padding:6px 16px" onclick="document.getElementById('bulkModal').style.display='none'">Annuler</button>
<button type="submit" class="btn-primary" style="padding:6px 20px" onclick="collectIds()">Appliquer</button>
</div>
</form>
</div>
</div>
<!-- Dry Run modal -->
<div id="dryRunModal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.7);z-index:100;align-items:center;justify-content:center" onclick="if(event.target===this)this.style.display='none'">
<div class="card" style="width:700px;max-width:90vw;padding:24px">
<div class="flex items-center justify-between mb-3">
<h3 style="color:#00ff88;font-weight:bold" id="dryRunTitle">Dry Run</h3>
<button id="copyBtn" onclick="copyDryRun()" class="btn-sm" style="background:#1e3a5f;color:#00d4ff;font-size:0.75rem;padding:4px 12px">Copier</button>
</div>
<pre id="dryRunCmd" style="background:#0a0e17;border:1px solid #1e3a5f;border-radius:6px;padding:12px;font-size:0.75rem;color:#00ff88;white-space:pre-wrap;word-break:break-all;max-height:400px;overflow-y:auto"></pre>
<div class="flex justify-end mt-3">
<button type="button" class="btn-sm" style="background:#333;color:#ccc;padding:6px 16px" onclick="document.getElementById('dryRunModal').style.display='none'">Fermer</button>
</div>
</div>
</div>
<script>
function showDetail(hostname, td) {
const tr = td.closest('tr');
const ge = tr.querySelector('input[name=general_excludes]').value.trim();
const se = tr.querySelector('input[name=specific_excludes]').value.trim();
document.getElementById('detailName').textContent = hostname;
document.getElementById('detailGeneral').textContent = ge ? ge.split(/\s+/).join('\n') : '(aucune)';
document.getElementById('detailSpecific').textContent = se ? se.split(/\s+/).join('\n') : '(aucune)';
const panel = document.getElementById('srvDetail');
panel.style.display = 'block';
panel.scrollIntoView({behavior: 'smooth', block: 'nearest'});
}
function showDryRun(hostname, btn) {
const tr = btn.closest('tr');
const ge = tr.querySelector('input[name=general_excludes]').value.trim();
const se = tr.querySelector('input[name=specific_excludes]').value.trim();
const all = (ge + ' ' + se).trim().split(/\s+/).filter(x => x);
const excludes = all.map(e => '--exclude=' + e).join(' \\\n ');
const cmd = 'yum update -y \\\n ' + excludes;
document.getElementById('dryRunTitle').textContent = 'Dry Run — ' + hostname;
document.getElementById('dryRunCmd').textContent = cmd;
document.getElementById('dryRunModal').style.display = 'flex';
}
function copyDryRun() {
const text = document.getElementById('dryRunCmd').textContent;
navigator.clipboard.writeText(text).then(() => {
const btn = document.getElementById('copyBtn');
btn.textContent = 'Copi\u00e9 !';
setTimeout(() => btn.textContent = 'Copier', 1500);
});
}
function toggleAll(cb) {
document.querySelectorAll('.srv-check').forEach(c => c.checked = cb.checked);
}
function collectIds() {
const ids = Array.from(document.querySelectorAll('.srv-check:checked')).map(c => c.value);
document.getElementById('bulkIds').value = ids.join(',');
}
</script>
{% endblock %}