patchcenter/app/templates/quickwin_correspondance.html
Khalid MOUTAOUAKIL e96d79aae3 QuickWin: prereq/snapshot services, referentiel, logs, correspondance
- Split quickwin services: prereq, snapshot, log services
- Add referentiel router and template
- QuickWin detail: prereq/snapshot terminal divs for production
- Server edit partial updates
- QuickWin correspondance and logs templates
- Base template updates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 18:13:00 +02:00

259 lines
13 KiB
HTML

{% extends "base.html" %}
{% block title %}Correspondance QuickWin #{{ run.id }}{% endblock %}
{% macro qs(pg=page) -%}
?page={{ pg }}&per_page={{ per_page }}&search={{ filters.search or '' }}&pair_filter={{ filters.pair_filter or '' }}&domain_filter={{ filters.domain_filter or '' }}&env_filter={{ filters.env_filter or '' }}
{%- endmacro %}
{% block content %}
<div class="flex items-center justify-between mb-4">
<div>
<a href="/quickwin/{{ run.id }}" class="text-xs text-gray-500 hover:text-gray-300">&larr; Retour campagne</a>
<h1 class="text-xl font-bold" style="color:#a78bfa">Correspondance H-Prod &harr; Prod</h1>
<p class="text-xs text-gray-500">{{ run.label }} &mdash; Appariement des serveurs hors-production avec leur homologue production</p>
</div>
<div class="flex gap-2 items-center">
<form method="post" action="/quickwin/{{ run.id }}/correspondance/auto">
<button class="btn-primary" style="padding:5px 16px;font-size:0.85rem">Auto-apparier</button>
</form>
<form method="post" action="/quickwin/{{ run.id }}/correspondance/clear-all"
onsubmit="return confirm('Supprimer tous les appariements ?')">
<button class="btn-sm btn-danger" style="padding:4px 12px">Tout effacer</button>
</form>
</div>
</div>
{% if msg %}
{% if msg == 'auto' %}
{% set am = request.query_params.get('am', '0') %}
{% set au = request.query_params.get('au', '0') %}
{% set aa = request.query_params.get('aa', '0') %}
<div style="background:#1a5a2e;color:#8f8;padding:8px 16px;border-radius:6px;margin-bottom:12px;font-size:0.85rem">
Auto-appariement termin&eacute; : {{ am }} appari&eacute;(s), {{ au }} sans homologue, {{ aa }} anomalie(s)
</div>
{% elif msg == 'cleared' %}
<div style="background:#5a3a1a;color:#ffcc00;padding:8px 16px;border-radius:6px;margin-bottom:12px;font-size:0.85rem">
Tous les appariements ont &eacute;t&eacute; supprim&eacute;s.
</div>
{% elif msg == 'bulk' %}
{% set bc = request.query_params.get('bc', '0') %}
<div style="background:#1a5a2e;color:#8f8;padding:8px 16px;border-radius:6px;margin-bottom:12px;font-size:0.85rem">
{{ bc }} appariement(s) modifi&eacute;(s) en masse.
</div>
{% endif %}
{% endif %}
<!-- KPIs -->
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:16px">
<div class="card p-3 text-center">
<div class="text-2xl font-bold" style="color:#fff">{{ stats.total }}</div>
<div class="text-xs text-gray-500">Total H-Prod</div>
</div>
<div class="card p-3 text-center">
<div class="text-2xl font-bold" style="color:#00ff88">{{ stats.matched }}</div>
<div class="text-xs text-gray-500">Appari&eacute;s</div>
</div>
<div class="card p-3 text-center">
<div class="text-2xl font-bold" style="color:#ffcc00">{{ stats.unmatched }}</div>
<div class="text-xs text-gray-500">Sans homologue</div>
</div>
<div class="card p-3 text-center">
<div class="text-2xl font-bold" style="color:#ff3366">{{ stats.anomalies }}</div>
<div class="text-xs text-gray-500">Anomalies</div>
</div>
</div>
<!-- Filtres -->
<form method="GET" class="card mb-4" style="padding:10px 16px;display:flex;gap:12px;align-items:center;flex-wrap:wrap">
<input type="text" name="search" value="{{ filters.search or '' }}" placeholder="Recherche hostname..." style="width:200px">
<select name="pair_filter" onchange="this.form.submit()" style="width:160px">
<option value="">Tous</option>
<option value="matched" {% if filters.pair_filter == 'matched' %}selected{% endif %}>Appari&eacute;s</option>
<option value="unmatched" {% if filters.pair_filter == 'unmatched' %}selected{% endif %}>Sans homologue</option>
<option value="anomaly" {% if filters.pair_filter == 'anomaly' %}selected{% endif %}>Anomalies</option>
</select>
<select name="domain_filter" onchange="this.form.submit()" style="width:150px">
<option value="">Tous domaines</option>
{% for d in domains_in_run %}
<option value="{{ d }}" {% if filters.domain_filter == d %}selected{% endif %}>{{ d }}</option>
{% endfor %}
</select>
<select name="env_filter" onchange="this.form.submit()" style="width:140px">
<option value="">Tous envs</option>
<option value="preprod" {% if filters.env_filter == 'preprod' %}selected{% endif %}>Pr&eacute;-Prod</option>
<option value="recette" {% if filters.env_filter == 'recette' %}selected{% endif %}>Recette</option>
<option value="dev" {% if filters.env_filter == 'dev' %}selected{% endif %}>D&eacute;veloppement</option>
<option value="test" {% if filters.env_filter == 'test' %}selected{% endif %}>Test</option>
</select>
<select name="per_page" onchange="this.form.submit()" style="width:70px">
{% for n in [20,50,100,200] %}
<option value="{{ n }}" {% if per_page == n %}selected{% endif %}>{{ n }}</option>
{% endfor %}
</select>
<button type="submit" class="btn-primary" style="padding:4px 14px;font-size:0.8rem">Filtrer</button>
<a href="/quickwin/{{ run.id }}/correspondance" class="text-xs text-gray-500 hover:text-gray-300">Reset</a>
<span class="text-xs text-gray-500" style="margin-left:auto">{{ total_filtered }} r&eacute;sultat(s)</span>
</form>
<!-- Actions en masse -->
<div class="card mb-3" style="padding:8px 16px;display:flex;gap:10px;align-items:center">
<span class="text-xs text-gray-400"><span id="sel-count">0</span> s&eacute;lectionn&eacute;(s)</span>
<button class="btn-sm" style="background:#ff336622;color:#ff3366;padding:3px 12px" onclick="bulkClear()">Dissocier la s&eacute;lection</button>
<span style="color:#1e3a5f">|</span>
<span class="text-xs text-gray-400">Associer la s&eacute;lection &agrave; :</span>
<select id="bulk-prod" style="width:220px;font-size:0.8rem;padding:3px 6px">
<option value="">-- Serveur prod --</option>
{% for a in available %}
<option value="{{ a.id }}">{{ a.hostname }}{% if a.domaine %} ({{ a.domaine }}){% endif %}</option>
{% endfor %}
</select>
<button class="btn-sm" style="background:#00ff8822;color:#00ff88;padding:3px 12px" onclick="bulkAssign()">Associer</button>
</div>
<!-- Table -->
<div class="card">
<div class="table-wrap" style="max-height:65vh;overflow-y:auto">
<table class="table-cyber w-full">
<thead style="position:sticky;top:0;z-index:1"><tr>
<th class="px-1 py-2" style="width:28px"><input type="checkbox" id="check-all" title="Tout"></th>
<th class="px-2 py-2" style="width:160px">Serveur H-Prod</th>
<th class="px-2 py-2" style="width:100px">Domaine</th>
<th class="px-2 py-2" style="width:90px">Env</th>
<th class="px-2 py-2" style="width:160px">Candidat auto</th>
<th class="px-2 py-2" style="width:50px">Statut</th>
<th class="px-2 py-2">Serveur Prod appari&eacute;</th>
<th class="px-2 py-2" style="width:100px">Domaine Prod</th>
<th class="px-2 py-2" style="width:80px">Action</th>
</tr></thead>
<tbody>
{% for p in pairs %}
<tr id="row-{{ p.hprod_id }}" style="{% if p.is_anomaly %}background:#ff336610{% elif not p.is_matched %}background:#ffcc0008{% endif %}">
<td class="px-1 py-2"><input type="checkbox" class="row-check" value="{{ p.hprod_id }}"></td>
<td class="px-2 py-2 font-bold" style="color:#00d4ff">{{ p.hprod_hostname }}</td>
<td class="px-2 py-2 text-xs text-gray-400">{{ p.hprod_domaine }}</td>
<td class="px-2 py-2 text-xs">
{% if p.is_anomaly %}<span class="badge badge-red" title="Lettre 'p' mais class&eacute; hprod">{{ p.hprod_env or '?' }}</span>
{% else %}<span class="text-gray-400">{{ p.hprod_env }}</span>{% endif %}
</td>
<td class="px-2 py-2 text-xs text-gray-500">{{ p.candidate }}</td>
<td class="px-2 py-2 text-center">
{% if p.is_matched %}<span class="badge badge-green">OK</span>
{% elif p.is_anomaly %}<span class="badge badge-red">!</span>
{% else %}<span class="badge badge-yellow">--</span>{% endif %}
</td>
<td class="px-2 py-2">
{% if p.is_matched %}
<span style="color:#00ff88;font-weight:600">{{ p.prod_hostname }}</span>
{% else %}
<select class="prod-select" data-hprod="{{ p.hprod_id }}" style="width:100%;font-size:0.8rem;padding:3px 6px">
<option value="">-- Choisir serveur prod --</option>
{% for a in available %}
<option value="{{ a.id }}">{{ a.hostname }}{% if a.domaine %} ({{ a.domaine }}){% endif %}</option>
{% endfor %}
</select>
{% endif %}
</td>
<td class="px-2 py-2 text-xs text-gray-400">
{% if p.is_matched %}{{ p.prod_domaine }}{% endif %}
</td>
<td class="px-2 py-2 text-center">
{% if p.is_matched %}
<button class="btn-sm" style="background:#ff336622;color:#ff3366;padding:2px 8px"
onclick="clearPair({{ p.hprod_id }})">X</button>
{% else %}
<button class="btn-sm" style="background:#00ff8822;color:#00ff88;padding:2px 8px"
onclick="setPairFromSelect({{ p.hprod_id }})">OK</button>
{% endif %}
</td>
</tr>
{% endfor %}
{% if not pairs %}
<tr><td colspan="9" class="px-2 py-8 text-center text-gray-500">Aucun r&eacute;sultat{% if filters.search or filters.pair_filter %} pour ces filtres{% endif %}</td></tr>
{% endif %}
</tbody>
</table>
</div>
</div>
<!-- Pagination -->
{% if total_pages > 1 %}
<div style="display:flex;justify-content:center;gap:6px;margin-top:12px">
{% if page > 1 %}
<a href="/quickwin/{{ run.id }}/correspondance{{ qs(page - 1) }}" class="btn-sm" style="background:#1e3a5f;color:#94a3b8;padding:4px 10px">&larr;</a>
{% endif %}
{% for pg in range(1, total_pages + 1) %}
{% if pg == page %}
<span class="btn-sm" style="background:#00d4ff;color:#0a0e17;padding:4px 10px;font-weight:bold">{{ pg }}</span>
{% elif pg <= 3 or pg >= total_pages - 1 or (pg >= page - 1 and pg <= page + 1) %}
<a href="/quickwin/{{ run.id }}/correspondance{{ qs(pg) }}" class="btn-sm" style="background:#1e3a5f;color:#94a3b8;padding:4px 10px">{{ pg }}</a>
{% elif pg == 4 or pg == total_pages - 2 %}
<span class="text-gray-500" style="padding:4px 4px">&hellip;</span>
{% endif %}
{% endfor %}
{% if page < total_pages %}
<a href="/quickwin/{{ run.id }}/correspondance{{ qs(page + 1) }}" class="btn-sm" style="background:#1e3a5f;color:#94a3b8;padding:4px 10px">&rarr;</a>
{% endif %}
</div>
{% endif %}
<script>
/* ---- Select all / count ---- */
const checkAll = document.getElementById('check-all');
if (checkAll) {
checkAll.addEventListener('change', function() {
document.querySelectorAll('.row-check').forEach(cb => cb.checked = this.checked);
updateSelCount();
});
}
document.querySelectorAll('.row-check').forEach(cb => cb.addEventListener('change', updateSelCount));
function updateSelCount() {
document.getElementById('sel-count').textContent = document.querySelectorAll('.row-check:checked').length;
}
/* ---- Single actions ---- */
function setPairFromSelect(hprodId) {
const sel = document.querySelector('select[data-hprod="' + hprodId + '"]');
if (!sel) return;
const prodId = parseInt(sel.value);
if (!prodId) { alert('Choisissez un serveur prod'); return; }
apiSetPair(hprodId, prodId).then(() => location.reload());
}
function clearPair(hprodId) {
if (!confirm('Dissocier cet appariement ?')) return;
apiSetPair(hprodId, 0).then(() => location.reload());
}
/* ---- Bulk actions ---- */
function getSelected() {
return [...document.querySelectorAll('.row-check:checked')].map(cb => parseInt(cb.value));
}
function bulkClear() {
const ids = getSelected();
if (!ids.length) { alert('Aucune ligne sélectionnée'); return; }
if (!confirm('Dissocier ' + ids.length + ' appariement(s) ?')) return;
Promise.all(ids.map(id => apiSetPair(id, 0))).then(() => {
location.href = '/quickwin/{{ run.id }}/correspondance?msg=bulk&bc=' + ids.length;
});
}
function bulkAssign() {
const ids = getSelected();
if (!ids.length) { alert('Aucune ligne sélectionnée'); return; }
const prodId = parseInt(document.getElementById('bulk-prod').value);
if (!prodId) { alert('Choisissez un serveur prod'); return; }
if (!confirm('Associer ' + ids.length + ' serveur(s) au même prod ?')) return;
Promise.all(ids.map(id => apiSetPair(id, prodId))).then(() => {
location.href = '/quickwin/{{ run.id }}/correspondance?msg=bulk&bc=' + ids.length;
});
}
/* ---- API call ---- */
function apiSetPair(hprodId, prodId) {
return fetch('/api/quickwin/correspondance/set-pair', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({hprod_id: hprodId, prod_id: prodId})
}).then(r => r.json());
}
</script>
{% endblock %}