patchcenter/app/templates/snapshots.html
Admin MPCZ 0b98b54a63 fix(snapshots): visibilite des filtres (compteur snapshots caches + message clair quand filtres trop restrictifs)
- Si les filtres cachent des snapshots, status affiche '⚠ N snapshot(s) cache(s)'
  avec hint pour decocher 'Mes snapshots' ou mettre age=0
- Tableau vide differencie 2 cas:
  * 0 trouvé total -> 'Verifie credentials vCenter'
  * X trouvé mais tous filtrés -> 'Ajuste les filtres ci-dessus' en jaune
2026-05-07 20:28:25 +02:00

256 lines
12 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 %}Snapshots VM{% endblock %}
{% block content %}
<div class="flex justify-between items-center mb-4">
<div>
<h2 class="text-xl font-bold text-cyber-accent">Gestion des snapshots VM</h2>
<p class="text-xs text-gray-500 mt-1">
Liste les snapshots existants sur les vCenters configurés. Filtre par auteur (préfixe nom = intervenant) et âge.
La suppression est définitive — confirmation requise.
</p>
</div>
</div>
<style>
.ss-row.dim { opacity: 0.4; }
.ss-row.selected { background: rgba(245,158,11,.1); }
.badge-age-old { background: rgba(239,68,68,.20); color: #ef4444; border: 1px solid #ef4444; }
.badge-age-warn { background: rgba(245,158,11,.20); color: #f59e0b; border: 1px solid #f59e0b; }
.badge-age-fresh { background: rgba(34,197,94,.20); color: #22c55e; border: 1px solid #22c55e; }
.filters-card label { display: flex; align-items: center; gap: 0.5rem; }
</style>
<div class="card p-4 mb-4 filters-card">
<div class="grid grid-cols-12 gap-3 items-end">
<div class="col-span-3">
<label class="text-xs text-gray-500">vCenter</label>
<select id="f-vcenter" class="w-full">
<option value="">— Tous les vCenters actifs —</option>
{% for vc in vcenters %}
<option value="{{ vc.id }}">{{ vc.name }} ({{ vc.endpoint }})</option>
{% endfor %}
</select>
</div>
<div class="col-span-3">
<label class="text-xs text-gray-500">Auteur (préfixe nom)</label>
<input type="text" id="f-author" value="{{ intervenant_default }}" placeholder="ex: khalid" class="w-full">
<label class="text-xs text-gray-400 mt-1"><input type="checkbox" id="f-only-mine" checked> Mes snapshots uniquement</label>
</div>
<div class="col-span-3">
<label class="text-xs text-gray-500">Âge minimum (jours)</label>
<input type="number" id="f-min-age" value="3" min="0" class="w-full">
</div>
<div class="col-span-3 flex gap-2 items-end">
<button id="btn-refresh" class="btn-action btn-pre" type="button" style="
padding: 6px 14px; font-size: 0.8rem; font-weight: 700;
border-radius: 6px; cursor: pointer; border: 1px solid #f59e0b;
background: rgba(245,158,11,.18); color: #f59e0b;
box-shadow: 0 0 8px #f59e0b; text-transform: uppercase;">
⟳ Charger / Rafraîchir
</button>
{% if can_delete %}
<button id="btn-delete" type="button" style="
padding: 6px 14px; font-size: 0.8rem; font-weight: 700;
border-radius: 6px; cursor: pointer; border: 1px solid #ef4444;
background: rgba(239,68,68,.18); color: #ef4444;
box-shadow: 0 0 8px #ef4444; text-transform: uppercase;
opacity: 0.5;">
✕ Supprimer la sélection
</button>
{% endif %}
</div>
</div>
</div>
<div id="status" class="mb-3 text-xs text-gray-400">Cliquer "Charger" pour scanner les vCenters.</div>
<div class="card overflow-hidden">
<table class="w-full text-xs" id="snap-table">
<thead class="text-cyber-accent border-b border-cyber-border">
<tr>
<th class="p-2 text-left"><input type="checkbox" id="sel-all"></th>
<th class="p-2 text-left">vCenter</th>
<th class="p-2 text-left">VM</th>
<th class="p-2 text-left">Snapshot</th>
<th class="p-2 text-left">Auteur</th>
<th class="p-2 text-left">Créé le</th>
<th class="p-2 text-left">Âge</th>
<th class="p-2 text-left">Description</th>
</tr>
</thead>
<tbody id="tbody"></tbody>
</table>
</div>
<script>
(function() {
const fVc = document.getElementById('f-vcenter');
const fAuthor = document.getElementById('f-author');
const fOnlyMine = document.getElementById('f-only-mine');
const fMinAge = document.getElementById('f-min-age');
const btnRefresh = document.getElementById('btn-refresh');
const btnDelete = document.getElementById('btn-delete');
const status = document.getElementById('status');
const tbody = document.getElementById('tbody');
const selAll = document.getElementById('sel-all');
let allSnaps = [];
function escapeHTML(s) {
return String(s||'').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[c]);
}
function ageBadge(ageDays) {
if (ageDays === null || ageDays === undefined) return '<span class="text-gray-500"></span>';
const d = Number(ageDays);
let cls = 'badge-age-fresh';
if (d >= 7) cls = 'badge-age-old';
else if (d >= 3) cls = 'badge-age-warn';
return `<span class="badge ${cls}">${d.toFixed(1)} j</span>`;
}
function applyFilters() {
const author = (fAuthor.value || '').trim().toLowerCase();
const onlyMine = fOnlyMine.checked;
const minAge = parseFloat(fMinAge.value) || 0;
return allSnaps.filter(s => {
if (onlyMine && author) {
if (!s.author || s.author.toLowerCase() !== author) return false;
}
if (minAge > 0 && (s.age_days === null || s.age_days < minAge)) return false;
return true;
});
}
function updateFilterSummary(visibleCount) {
// Affiche le nombre cachés vs total pour debug
const total = allSnaps.length;
const hidden = total - visibleCount;
const baseStatus = status.dataset.baseMsg || '';
if (hidden > 0) {
status.innerHTML = baseStatus + ` <span class="text-cyber-yellow">⚠ ${hidden} snapshot(s) caché(s) par les filtres</span> (décoche "Mes snapshots" ou mets âge=0 pour tout voir).`;
} else {
status.innerHTML = baseStatus;
}
}
function render() {
const rows = applyFilters();
updateFilterSummary(rows.length);
if (!rows.length) {
const total = allSnaps.length;
tbody.innerHTML = total === 0
? '<tr><td colspan="8" class="p-4 text-center text-gray-500">Aucun snapshot dans le scan. Vérifie credentials vCenter.</td></tr>'
: `<tr><td colspan="8" class="p-4 text-center text-cyber-yellow">${total} snapshot(s) trouvé(s) mais tous filtrés. Ajuste les filtres ci-dessus.</td></tr>`;
updateDeleteBtn();
return;
}
tbody.innerHTML = rows.map((s, idx) => `
<tr class="ss-row border-b border-cyber-border/30" data-idx="${allSnaps.indexOf(s)}">
<td class="p-2"><input type="checkbox" class="row-cb"></td>
<td class="p-2 font-mono">${escapeHTML(s.vcenter_name)}</td>
<td class="p-2 font-mono">${escapeHTML(s.vm_name)}</td>
<td class="p-2 font-mono">${escapeHTML(s.snap_name)}</td>
<td class="p-2">${escapeHTML(s.author || '<i class="text-gray-500">inconnu</i>')}</td>
<td class="p-2 font-mono text-[10px]">${escapeHTML(s.created_at || '')}</td>
<td class="p-2">${ageBadge(s.age_days)}</td>
<td class="p-2 text-gray-400">${escapeHTML(s.description || '')}</td>
</tr>
`).join('');
tbody.querySelectorAll('.row-cb').forEach(cb => {
cb.addEventListener('change', () => {
cb.closest('tr').classList.toggle('selected', cb.checked);
updateDeleteBtn();
});
});
updateDeleteBtn();
}
function updateDeleteBtn() {
if (!btnDelete) return;
const n = tbody.querySelectorAll('.row-cb:checked').length;
btnDelete.style.opacity = n > 0 ? '1' : '0.5';
btnDelete.textContent = n > 0 ? `✕ SUPPRIMER ${n} SNAPSHOT${n>1?'S':''}` : '✕ SUPPRIMER LA SÉLECTION';
}
btnRefresh.addEventListener('click', async () => {
status.textContent = 'Scan en cours… (peut prendre 10-30 s selon les vCenters)';
btnRefresh.disabled = true;
try {
const fd = new FormData();
if (fVc.value) fd.append('vcenter_id', fVc.value);
const r = await fetch('/snapshots/list', {method: 'POST', credentials: 'same-origin', body: fd});
const j = await r.json();
if (!j.ok) {
status.innerHTML = '<span class="text-cyber-red">Erreur : ' + escapeHTML(j.msg || 'inconnue') + '</span>';
return;
}
allSnaps = j.snapshots || [];
const errs = (j.errors || []).map(e => `${e.vcenter}: ${e.msg}`).join(' · ');
const baseMsg = `${j.snap_count} snapshot(s) trouvé(s) sur ${j.vcenter_count} vCenter(s).` +
(errs ? ` <span class="text-cyber-yellow">⚠ ${escapeHTML(errs)}</span>` : '');
status.dataset.baseMsg = baseMsg;
status.innerHTML = baseMsg;
render();
} catch (e) {
status.innerHTML = '<span class="text-cyber-red">Erreur réseau : ' + escapeHTML(String(e)) + '</span>';
} finally {
btnRefresh.disabled = false;
}
});
[fAuthor, fMinAge, fOnlyMine].forEach(el => el.addEventListener('input', render));
fOnlyMine.addEventListener('change', render);
selAll.addEventListener('change', () => {
tbody.querySelectorAll('.row-cb').forEach(cb => {
cb.checked = selAll.checked;
cb.closest('tr').classList.toggle('selected', cb.checked);
});
updateDeleteBtn();
});
if (btnDelete) btnDelete.addEventListener('click', async () => {
const checked = Array.from(tbody.querySelectorAll('.row-cb:checked'));
if (!checked.length) { alert('Sélectionne au moins un snapshot.'); return; }
const items = checked.map(cb => {
const idx = parseInt(cb.closest('tr').dataset.idx, 10);
const s = allSnaps[idx];
return {
vcenter_id: s.vcenter_id, vm_moid: s.vm_moid, snap_id: s.snap_id,
vm_name: s.vm_name, snap_name: s.snap_name,
};
});
const lines = items.slice(0, 15).map(i => `${i.vm_name}${i.snap_name}`).join('\n');
const more = items.length > 15 ? `\n … et ${items.length - 15} autre(s)` : '';
if (!confirm(`⚠ Supprimer ${items.length} snapshot(s) ? Cette action est DÉFINITIVE.\n\n${lines}${more}`)) return;
if (!confirm(`Confirme une 2e fois : suppression définitive de ${items.length} snapshot(s)`)) return;
status.textContent = 'Suppression en cours…';
btnDelete.disabled = true;
try {
const r = await fetch('/snapshots/delete', {
method: 'POST', credentials: 'same-origin',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({items}),
});
const j = await r.json();
const okCount = (j.results || []).filter(x => x.ok).length;
const failed = (j.results || []).filter(x => !x.ok);
let msg = `${okCount}/${items.length} supprimés.`;
if (failed.length) {
msg += '\n\nÉchecs :\n' + failed.slice(0,10).map(f => ` ${f.vm_name}${f.snap_name}: ${f.msg}`).join('\n');
}
alert(msg);
// Recharger
btnRefresh.click();
} catch (e) {
alert('Erreur réseau : ' + e);
} finally {
btnDelete.disabled = false;
}
});
})();
</script>
{% endblock %}