diff --git a/app/routers/snapshots.py b/app/routers/snapshots.py index 3720787..2846977 100644 --- a/app/routers/snapshots.py +++ b/app/routers/snapshots.py @@ -24,7 +24,7 @@ def _can_delete_snaps(perms): def _get_user_intervenant_name(db, user): """Retourne le nom utilisé comme préfixe dans les snapshots PatchCenter de cet utilisateur. - Priorité : settings 'patcher' > username.""" + Priorité : settings 'patcher' > username (sub du JWT) > display_name.""" try: from ..services.secrets_service import get_secret patcher = (get_secret(db, "patcher") or "").strip() @@ -32,7 +32,8 @@ def _get_user_intervenant_name(db, user): return patcher except Exception: pass - return user.get("username", "") + # JWT stocke le username dans 'sub' (cf auth.py) + return (user.get("sub") or user.get("username") or user.get("display_name") or "").strip() @router.get("/snapshots", response_class=HTMLResponse) diff --git a/app/services/snapshot_mgmt_service.py b/app/services/snapshot_mgmt_service.py index e3ec435..55c37fb 100644 --- a/app/services/snapshot_mgmt_service.py +++ b/app/services/snapshot_mgmt_service.py @@ -25,8 +25,9 @@ except ImportError: log.warning("pyvmomi non disponible — listing snapshots impossible") +# Format strict PatchCenter : `_YYYY-MM-DD_avant_patch` SNAP_NAME_RE = re.compile( - r"^(?P[A-Za-z0-9_\-\.]+)_(?P\d{4}-\d{2}-\d{2})(?:_(?P.+))?$" + r"^(?P[A-Za-z0-9_\-\.]+)_(?P\d{4}-\d{2}-\d{2})_avant_patch$" ) @@ -71,11 +72,14 @@ def _walk_snapshots(snapshot_list, vm, vcenter_name, vcenter_id, vm_moid, parent created_iso = str(created) age_days = None - # Auteur déduit du préfixe du nom (format PatchCenter) + # Auteur déduit du préfixe du nom (format strict PatchCenter) + # `_YYYY-MM-DD_avant_patch` author = None + is_pc_format = False m = SNAP_NAME_RE.match(name) if m: author = m.group("author") + is_pc_format = True yield { "vcenter_id": vcenter_id, @@ -89,6 +93,7 @@ def _walk_snapshots(snapshot_list, vm, vcenter_name, vcenter_id, vm_moid, parent "created_at": created_iso, "age_days": round(age_days, 2) if age_days is not None else None, "author": author, + "is_patchcenter_format": is_pc_format, "is_current": bool(getattr(s, "id", None) and vm.snapshot and vm.snapshot.currentSnapshot and vm.snapshot.currentSnapshot._moId == snap_moid), } diff --git a/app/templates/snapshots.html b/app/templates/snapshots.html index 9627ddb..5fcf759 100644 --- a/app/templates/snapshots.html +++ b/app/templates/snapshots.html @@ -35,6 +35,7 @@ +
@@ -87,6 +88,7 @@ const fVc = document.getElementById('f-vcenter'); const fAuthor = document.getElementById('f-author'); const fOnlyMine = document.getElementById('f-only-mine'); + const fPcFormat = document.getElementById('f-pc-format'); const fMinAge = document.getElementById('f-min-age'); const btnRefresh = document.getElementById('btn-refresh'); const btnDelete = document.getElementById('btn-delete'); @@ -112,16 +114,36 @@ function applyFilters() { const author = (fAuthor.value || '').trim().toLowerCase(); const onlyMine = fOnlyMine.checked; + const pcOnly = fPcFormat.checked; const minAge = parseFloat(fMinAge.value) || 0; return allSnaps.filter(s => { - if (onlyMine && author) { - if (!s.author || s.author.toLowerCase() !== author) return false; + // Format PatchCenter uniquement + if (pcOnly && !s.is_patchcenter_format) return false; + // Mes snapshots uniquement (auteur doit etre present et matcher) + if (onlyMine) { + if (!author) { + // pas d'auteur indique -> on prend uniquement ceux dont l'auteur est connu + if (!s.author) return false; + } else 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 fmtDateFR(iso) { + // ISO 8601 -> 'jj/mm/aaaa HH:MM' (heure locale du navigateur) + if (!iso) return '–'; + try { + const d = new Date(iso); + if (isNaN(d.getTime())) return iso; + const pad = n => String(n).padStart(2, '0'); + return `${pad(d.getDate())}/${pad(d.getMonth()+1)}/${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}`; + } catch (e) { return iso; } + } + function updateFilterSummary(visibleCount) { // Affiche le nombre cachés vs total pour debug const total = allSnaps.length; @@ -145,18 +167,22 @@ updateDeleteBtn(); return; } - tbody.innerHTML = rows.map((s, idx) => ` + tbody.innerHTML = rows.map((s, idx) => { + const authorCell = s.author + ? escapeHTML(s.author) + : 'inconnu'; // bypass escape pour ce litteral HTML + return ` ${escapeHTML(s.vcenter_name)} ${escapeHTML(s.vm_name)} ${escapeHTML(s.snap_name)} - ${escapeHTML(s.author || 'inconnu')} - ${escapeHTML(s.created_at || '–')} + ${authorCell} + ${escapeHTML(fmtDateFR(s.created_at))} ${ageBadge(s.age_days)} ${escapeHTML(s.description || '–')} - - `).join(''); + `; + }).join(''); tbody.querySelectorAll('.row-cb').forEach(cb => { cb.addEventListener('change', () => { cb.closest('tr').classList.toggle('selected', cb.checked); @@ -173,9 +199,23 @@ btnDelete.textContent = n > 0 ? `✕ SUPPRIMER ${n} SNAPSHOT${n>1?'S':''}` : '✕ SUPPRIMER LA SÉLECTION'; } + function setBusy(msg, btn) { + status.innerHTML = '⏳ ' + escapeHTML(msg) + ''; + if (btn) { + btn.disabled = true; + btn._origText = btn._origText || btn.textContent; + btn.textContent = '⏳ ' + msg; + } + } + function clearBusy(btn) { + if (btn && btn._origText) { + btn.disabled = false; + btn.textContent = btn._origText; + } + } + btnRefresh.addEventListener('click', async () => { - status.textContent = 'Scan en cours… (peut prendre 10-30 s selon les vCenters)'; - btnRefresh.disabled = true; + setBusy('Recherche en cours… (peut prendre 10-30 s selon les vCenters)', btnRefresh); try { const fd = new FormData(); if (fVc.value) fd.append('vcenter_id', fVc.value); @@ -195,12 +235,12 @@ } catch (e) { status.innerHTML = 'Erreur réseau : ' + escapeHTML(String(e)) + ''; } finally { - btnRefresh.disabled = false; + clearBusy(btnRefresh); } }); - [fAuthor, fMinAge, fOnlyMine].forEach(el => el.addEventListener('input', render)); - fOnlyMine.addEventListener('change', render); + [fAuthor, fMinAge, fOnlyMine, fPcFormat].forEach(el => el.addEventListener('input', render)); + [fOnlyMine, fPcFormat].forEach(el => el.addEventListener('change', render)); selAll.addEventListener('change', () => { tbody.querySelectorAll('.row-cb').forEach(cb => { @@ -226,8 +266,7 @@ 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; + setBusy(`Suppression en cours… (${items.length} snapshot(s))`, btnDelete); try { const r = await fetch('/snapshots/delete', { method: 'POST', credentials: 'same-origin', @@ -242,12 +281,12 @@ msg += '\n\nÉchecs :\n' + failed.slice(0,10).map(f => ` ${f.vm_name} → ${f.snap_name}: ${f.msg}`).join('\n'); } alert(msg); - // Recharger + clearBusy(btnDelete); + // Re-scanne pour refleter l'etat reel cote vCenter btnRefresh.click(); } catch (e) { alert('Erreur réseau : ' + e); - } finally { - btnDelete.disabled = false; + clearBusy(btnDelete); } }); })();