patchcenter/app/templates/patching_import.html

235 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends 'base.html' %}
{% block title %}Importer planning patching{% endblock %}
{% block content %}
<div class="flex justify-between items-center mb-4">
<div>
<h2 class="text-xl font-bold text-cyber-accent">Importer planning de patching</h2>
<p class="text-xs text-gray-500 mt-1">
Upload du fichier Excel "Plan de Patching serveurs YYYY". Une feuille = une semaine (S02..S52).
Les onglets historiques (Histo-XXXX) sont ignorés.
</p>
</div>
{% if current_import %}
<a href="/patching/import" class="btn-sm bg-cyber-border text-cyber-accent px-4 py-2">← Liste imports</a>
{% endif %}
</div>
{% if msg == 'ok' %}<div class="bg-cyber-green/20 text-cyber-green p-2 mb-3 text-xs rounded">Import réussi.</div>{% endif %}
{% if msg == 'deleted' %}<div class="bg-cyber-blue/20 text-cyber-blue p-2 mb-3 text-xs rounded">Import supprimé.</div>{% endif %}
{% if err == 'ext' %}<div class="bg-cyber-red/20 text-cyber-red p-2 mb-3 text-xs rounded">Le fichier doit être un .xlsx.</div>{% endif %}
{% if err == 'parse' %}<div class="bg-cyber-red/20 text-cyber-red p-2 mb-3 text-xs rounded">Impossible de parser le fichier.</div>{% endif %}
{% if err == 'denied' %}<div class="bg-cyber-red/20 text-cyber-red p-2 mb-3 text-xs rounded">Permission refusée.</div>{% endif %}
{% if err == 'notfound' %}<div class="bg-cyber-red/20 text-cyber-red p-2 mb-3 text-xs rounded">Import introuvable.</div>{% endif %}
{% if err == 'openpyxl_missing' %}<div class="bg-cyber-red/20 text-cyber-red p-2 mb-3 text-xs rounded">Lib openpyxl manquante côté serveur.</div>{% endif %}
{# ──────────── Upload form ──────────── #}
{% if can_import %}
<div class="card p-4 mb-4">
<h3 class="text-sm font-bold text-cyber-accent mb-2">Nouvel import</h3>
<form method="POST" action="/patching/import/upload" enctype="multipart/form-data" class="flex flex-wrap gap-2 items-center">
<input type="file" name="file" accept=".xlsx" required class="text-xs">
<input type="text" name="note" placeholder="Note (optionnelle)" class="text-xs px-2 py-1 flex-1 min-w-[200px]">
<button type="submit" class="btn-primary px-3 py-1 text-xs">Importer</button>
</form>
</div>
{% endif %}
{# ──────────── Liste des imports ──────────── #}
<div class="card p-3 mb-4">
<h3 class="text-sm font-bold text-cyber-accent mb-2">Imports récents ({{ imports|length }})</h3>
{% if imports %}
<table class="w-full text-xs">
<thead class="text-cyber-accent border-b border-cyber-border">
<tr>
<th class="text-left p-1">ID</th>
<th class="text-left p-1">Fichier</th>
<th class="text-left p-1">Année</th>
<th class="text-right p-1">Feuilles</th>
<th class="text-right p-1">Lignes</th>
<th class="text-left p-1">Date</th>
<th class="text-left p-1">Par</th>
<th class="text-right p-1">Actions</th>
</tr>
</thead>
<tbody>
{% for i in imports %}
<tr class="border-b border-cyber-border/30 {% if current_import and current_import.id == i.id %}bg-cyber-border/30{% endif %}">
<td class="p-1">#{{ i.id }}</td>
<td class="p-1"><a href="/patching/import/{{ i.id }}" class="text-cyber-accent hover:underline">{{ i.filename }}</a></td>
<td class="p-1">{{ i.year or '' }}</td>
<td class="p-1 text-right">{{ i.sheet_count }}</td>
<td class="p-1 text-right">{{ i.row_count }}</td>
<td class="p-1">{{ i.uploaded_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td class="p-1">{{ i.uploaded_by_name or '' }}</td>
<td class="p-1 text-right">
<a href="/patching/import/{{ i.id }}" class="text-cyber-blue hover:underline">Voir</a>
{% if can_import %}
· <form method="POST" action="/patching/import/{{ i.id }}/delete" class="inline" onsubmit="return confirm('Supprimer cet import ?')">
<button type="submit" class="text-cyber-red hover:underline">Suppr</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="text-xs text-gray-500">Aucun import pour le moment.</p>
{% endif %}
</div>
{# ──────────── Détail de l'import courant ──────────── #}
{% if current_import %}
<div class="card p-4 mb-4">
<div class="flex justify-between items-start mb-3">
<div>
<h3 class="text-sm font-bold text-cyber-accent">Import #{{ current_import.id }} : {{ current_import.filename }}</h3>
<p class="text-xs text-gray-500 mt-1">
{{ current_import.sheet_count }} feuilles · {{ current_import.row_count }} lignes ·
{{ current_import.uploaded_at.strftime('%Y-%m-%d %H:%M') }}
{% if current_import.note %} · <em>{{ current_import.note }}</em>{% endif %}
</p>
</div>
</div>
{# Sélecteur de semaine #}
<div class="flex gap-2 items-center mb-3 flex-wrap">
<label class="text-xs text-gray-400">Semaine :</label>
<select id="sheet-select" class="text-xs px-2 py-1">
<option value="">— Choisir —</option>
{% for s in sheets %}
<option value="{{ s.sheet_name }}">{{ s.sheet_name }} (S{{ '%02d' % s.week_number }}) — {{ s.nb }} serveur(s)</option>
{% endfor %}
</select>
<span id="sheet-summary" class="text-xs text-gray-500"></span>
</div>
{# Tableau dynamique #}
<div id="sheet-table-wrap" class="overflow-x-auto" style="display:none;">
<div class="flex gap-2 items-center mb-2 flex-wrap">
<label class="text-xs">
<input type="checkbox" id="select-all" class="mr-1"> Tout sélectionner
</label>
<span class="text-xs text-gray-400" id="selection-count">0 sélectionné(s)</span>
<div class="flex-1"></div>
<button id="btn-prepatch" class="btn-sm bg-cyber-yellow/20 text-cyber-yellow px-3 py-1 text-xs" disabled>
Pré-patching <span class="text-[10px] opacity-60">(étape 2)</span>
</button>
<button id="btn-patch" class="btn-sm bg-cyber-green/20 text-cyber-green px-3 py-1 text-xs" disabled>
Patcher <span class="text-[10px] opacity-60">(étape 3)</span>
</button>
</div>
<table class="w-full text-xs" id="sheet-table">
<thead class="text-cyber-accent border-b border-cyber-border">
<tr>
<th class="text-left p-1 w-6"><input type="checkbox" id="select-all-head"></th>
<th class="text-left p-1">Asset</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">Application</th>
<th class="text-left p-1">Intervenant</th>
<th class="text-left p-1">Valideur RA</th>
<th class="text-left p-1">Jour</th>
<th class="text-left p-1">Heure</th>
<th class="text-left p-1">Coupure</th>
<th class="text-left p-1">Pb disque</th>
<th class="text-left p-1">Lien serveur</th>
</tr>
</thead>
<tbody id="sheet-table-body"></tbody>
</table>
</div>
<div id="sheet-empty" class="text-xs text-gray-500" style="display:none;">Aucune ligne pour cette feuille.</div>
</div>
<script>
(function(){
const importId = {{ current_import.id }};
const sel = document.getElementById('sheet-select');
const wrap = document.getElementById('sheet-table-wrap');
const empty = document.getElementById('sheet-empty');
const tbody = document.getElementById('sheet-table-body');
const summary = document.getElementById('sheet-summary');
const selAll = document.getElementById('select-all');
const selAllHead = document.getElementById('select-all-head');
const selCount = document.getElementById('selection-count');
const btnPre = document.getElementById('btn-prepatch');
const btnPatch = document.getElementById('btn-patch');
function escapeHTML(s){
if (s === null || s === undefined) return '';
return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
function refreshSelection(){
const checked = tbody.querySelectorAll('input.row-cb:checked').length;
const total = tbody.querySelectorAll('input.row-cb').length;
selCount.textContent = checked + ' / ' + total + ' sélectionné(s)';
selAll.checked = (checked > 0 && checked === total);
selAllHead.checked = selAll.checked;
// Pré-patching et patch désactivés tant que les étapes 2/3 ne sont pas faites
// mais on prépare la condition pour la suite :
const hasSel = checked > 0;
btnPre.disabled = !hasSel;
btnPatch.disabled = !hasSel;
}
async function loadSheet(name){
if (!name) { wrap.style.display='none'; empty.style.display='none'; return; }
summary.textContent = 'Chargement…';
const r = await fetch('/patching/import/' + importId + '/sheet/' + encodeURIComponent(name));
const j = await r.json();
if (!j.ok) { summary.textContent = j.msg || 'Erreur'; return; }
if (!j.rows.length) {
wrap.style.display='none'; empty.style.display=''; summary.textContent='';
return;
}
empty.style.display='none'; wrap.style.display='';
summary.textContent = j.count + ' lignes';
tbody.innerHTML = j.rows.map(r => {
const linkSrv = r.server_id
? '<a href="/qualys/search?field=hostname&q=' + encodeURIComponent(r.resolved_hostname || r.asset_name) + '" class="text-cyber-blue hover:underline">' + escapeHTML(r.resolved_hostname || r.asset_name) + '</a>'
: '<span class="text-cyber-yellow" title="Pas matché en base PatchCenter">' + escapeHTML(r.asset_name || '') + ' ⚠</span>';
const pb = r.pb_espace_disque === true ? '<span class="text-cyber-red">⚠ Oui</span>' : (r.pb_espace_disque === false ? 'Non' : '');
return '<tr class="border-b border-cyber-border/20 hover:bg-cyber-border/10">'
+ '<td class="p-1"><input type="checkbox" class="row-cb" data-id="' + r.id + '" data-asset="' + escapeHTML(r.asset_name||'') + '" data-server-id="' + (r.server_id||'') + '"></td>'
+ '<td class="p-1 font-mono">' + escapeHTML(r.asset_name||'') + '</td>'
+ '<td class="p-1">' + escapeHTML(r.environnement||'') + '</td>'
+ '<td class="p-1">' + escapeHTML(r.domaine||'') + '</td>'
+ '<td class="p-1">' + escapeHTML(r.os||'') + '</td>'
+ '<td class="p-1">' + escapeHTML(r.application_name||'') + '</td>'
+ '<td class="p-1">' + escapeHTML(r.intervenant||'') + '</td>'
+ '<td class="p-1">' + escapeHTML(r.valideur_ra||'') + '</td>'
+ '<td class="p-1">' + escapeHTML(r.jour||'') + '</td>'
+ '<td class="p-1">' + escapeHTML(r.heure||'') + '</td>'
+ '<td class="p-1">' + escapeHTML(r.duree_coupure||'') + '</td>'
+ '<td class="p-1">' + pb + '</td>'
+ '<td class="p-1">' + linkSrv + '</td>'
+ '</tr>';
}).join('');
tbody.querySelectorAll('input.row-cb').forEach(cb => cb.addEventListener('change', refreshSelection));
refreshSelection();
}
sel.addEventListener('change', () => loadSheet(sel.value));
function toggleAll(state){
tbody.querySelectorAll('input.row-cb').forEach(cb => cb.checked = state);
refreshSelection();
}
selAll.addEventListener('change', () => toggleAll(selAll.checked));
selAllHead.addEventListener('change', () => toggleAll(selAllHead.checked));
btnPre.addEventListener('click', () => {
const ids = Array.from(tbody.querySelectorAll('input.row-cb:checked')).map(cb => cb.dataset.serverId).filter(x => x);
alert('Pré-patching à brancher (étape 2) — ' + ids.length + ' serveur(s) résolu(s) en base.');
});
btnPatch.addEventListener('click', () => {
const ids = Array.from(tbody.querySelectorAll('input.row-cb:checked')).map(cb => cb.dataset.serverId).filter(x => x);
alert('Patching by-step à brancher (étape 3) — ' + ids.length + ' serveur(s) résolu(s) en base.');
});
})();
</script>
{% endif %}
{% endblock %}