feat(snapshots): UI simplifiee - dropdown Intervenant unique (defaut user connecte)

Avant: 3 champs (Auteur input + 2 checkboxes 'Mes snapshots' / 'Format PatchCenter') -> trop.

Apres:
- Un seul dropdown 'Intervenant' avec liste dynamique des auteurs detectes dans les
  snapshots PatchCenter charges
- Defaut = user connecte (intervenant_default depuis JWT 'sub')
- Ajoute '(toi)' a cote de l'option qui matche le user connecte
- Si le user connecte n'a pas encore de snapshot, son option apparait quand meme en tete
  avec le tag '(toi, aucun snap)'
- Le filtre 'PatchCenter uniquement' est implicite (toujours actif) -> les snapshots
  SLPM ou manuels n'apparaissent plus du tout
- Les autres users peuvent etre selectionnes pour afficher/supprimer leurs snapshots
  (pas de restriction par compte au-dela de l'authentification)
- updateFilterSummary affiche le filtre actif clairement
This commit is contained in:
Pierre & Lumière 2026-05-07 21:00:28 +02:00
parent c918edb093
commit 46b80474c2

View File

@ -56,10 +56,11 @@
</select> </select>
</div> </div>
<div class="col-span-3"> <div class="col-span-3">
<label class="text-xs text-gray-500">Auteur (préfixe nom)</label> <label class="text-xs text-gray-500">Intervenant</label>
<input type="text" id="f-author" value="{{ intervenant_default }}" placeholder="ex: khalid" class="w-full"> <select id="f-intervenant" class="w-full">
<label class="text-xs text-gray-400 mt-1"><input type="checkbox" id="f-only-mine" checked> Mes snapshots uniquement (login = auteur)</label> <option value="{{ intervenant_default }}" selected>{{ intervenant_default or '(toi)' }}</option>
<label class="text-xs text-gray-400 mt-1"><input type="checkbox" id="f-only-pc" checked> Snapshots PatchCenter uniquement (exclut SLPM .exe et manuels)</label> </select>
<p class="text-xs text-gray-500 mt-1">Liste alimentée après chargement (auteurs détectés dans les snapshots PatchCenter).</p>
</div> </div>
<div class="col-span-3"> <div class="col-span-3">
<label class="text-xs text-gray-500">Âge minimum (jours)</label> <label class="text-xs text-gray-500">Âge minimum (jours)</label>
@ -111,10 +112,9 @@
<script> <script>
(function() { (function() {
const fVc = document.getElementById('f-vcenter'); const fVc = document.getElementById('f-vcenter');
const fAuthor = document.getElementById('f-author'); const fIntervenant = document.getElementById('f-intervenant');
const fOnlyMine = document.getElementById('f-only-mine');
const fOnlyPc = document.getElementById('f-only-pc');
const fMinAge = document.getElementById('f-min-age'); const fMinAge = document.getElementById('f-min-age');
const intervenantDefault = "{{ intervenant_default|e }}";
const btnRefresh = document.getElementById('btn-refresh'); const btnRefresh = document.getElementById('btn-refresh');
const btnDelete = document.getElementById('btn-delete'); const btnDelete = document.getElementById('btn-delete');
const status = document.getElementById('status'); const status = document.getElementById('status');
@ -137,26 +137,43 @@
} }
function applyFilters() { function applyFilters() {
const author = (fAuthor.value || '').trim().toLowerCase(); // Toujours: snapshots PatchCenter uniquement (origin === 'patchcenter')
const onlyMine = fOnlyMine.checked; // + intervenant choisi dans le dropdown
const pcOnly = fOnlyPc.checked; // + age minimum
const intervenant = (fIntervenant.value || '').trim().toLowerCase();
const minAge = parseFloat(fMinAge.value) || 0; const minAge = parseFloat(fMinAge.value) || 0;
return allSnaps.filter(s => { return allSnaps.filter(s => {
// Snapshots PatchCenter uniquement (origin === 'patchcenter') if (s.origin !== 'patchcenter') return false;
if (pcOnly && s.origin !== 'patchcenter') return false; if (intervenant && (!s.author || s.author.toLowerCase() !== intervenant)) return false;
// Mes snapshots uniquement
if (onlyMine) {
if (!author) {
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; if (minAge > 0 && (s.age_days === null || s.age_days < minAge)) return false;
return true; return true;
}); });
} }
function rebuildIntervenantDropdown() {
// Liste des auteurs distincts des snapshots PatchCenter trouvés
const authors = Array.from(new Set(
allSnaps.filter(s => s.origin === 'patchcenter' && s.author).map(s => s.author)
)).sort((a,b) => a.localeCompare(b, 'fr', {sensitivity:'base'}));
// Conserve la sélection courante (ou défaut user connecté)
const current = fIntervenant.value || intervenantDefault;
let opts = '';
// Si user connecté pas dans la liste (= aucun snap encore), on l'ajoute en tête
const haveCurrent = authors.some(a => a.toLowerCase() === current.toLowerCase());
if (intervenantDefault && !authors.some(a => a.toLowerCase() === intervenantDefault.toLowerCase())) {
opts += `<option value="${intervenantDefault}">${intervenantDefault} (toi, aucun snap)</option>`;
}
for (const a of authors) {
const isYou = a.toLowerCase() === intervenantDefault.toLowerCase();
const sel = (a.toLowerCase() === current.toLowerCase()) ? ' selected' : '';
opts += `<option value="${a}"${sel}>${a}${isYou ? ' (toi)' : ''}</option>`;
}
if (!opts) opts = '<option value="">(aucun snap PatchCenter trouvé)</option>';
fIntervenant.innerHTML = opts;
// Re-sélectionne current ou défaut si présent
if (haveCurrent) fIntervenant.value = current;
}
function fmtDateFR(iso) { function fmtDateFR(iso) {
// ISO 8601 -> 'jj/mm/aaaa HH:MM' (heure locale du navigateur) // ISO 8601 -> 'jj/mm/aaaa HH:MM' (heure locale du navigateur)
if (!iso) return ''; if (!iso) return '';
@ -169,15 +186,12 @@
} }
function updateFilterSummary(visibleCount) { function updateFilterSummary(visibleCount) {
// Affiche le nombre cachés vs total pour debug // Affiche combien sont cachés et la cible
const total = allSnaps.length;
const hidden = total - visibleCount;
const baseStatus = status.dataset.baseMsg || ''; const baseStatus = status.dataset.baseMsg || '';
if (hidden > 0) { const intervenant = (fIntervenant.value || '').trim();
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).`; const minAge = parseFloat(fMinAge.value) || 0;
} else { const filterDesc = `intervenant=${intervenant || 'tous'}` + (minAge > 0 ? `, âge≥${minAge}j` : '');
status.innerHTML = baseStatus; status.innerHTML = baseStatus + ` <span class="text-cyber-accent">→ ${visibleCount} affiché(s) (filtre : ${escapeHTML(filterDesc)})</span>`;
}
} }
function render() { function render() {
@ -277,10 +291,12 @@
} }
allSnaps = j.snapshots || []; allSnaps = j.snapshots || [];
const errs = (j.errors || []).map(e => `${e.vcenter}: ${e.msg}`).join(' · '); 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).` + const pcCount = allSnaps.filter(s => s.origin === 'patchcenter').length;
const baseMsg = `${j.snap_count} snapshot(s) trouvé(s) sur ${j.vcenter_count} vCenter(s) (dont ${pcCount} PatchCenter).` +
(errs ? ` <span class="text-cyber-yellow">⚠ ${escapeHTML(errs)}</span>` : ''); (errs ? ` <span class="text-cyber-yellow">⚠ ${escapeHTML(errs)}</span>` : '');
status.dataset.baseMsg = baseMsg; status.dataset.baseMsg = baseMsg;
status.innerHTML = baseMsg; status.innerHTML = baseMsg;
rebuildIntervenantDropdown();
render(); render();
} catch (e) { } catch (e) {
status.innerHTML = '<span class="text-cyber-red">Erreur réseau : ' + escapeHTML(String(e)) + '</span>'; status.innerHTML = '<span class="text-cyber-red">Erreur réseau : ' + escapeHTML(String(e)) + '</span>';
@ -289,8 +305,8 @@
} }
}); });
[fAuthor, fMinAge, fOnlyMine, fOnlyPc].forEach(el => el.addEventListener('input', render)); [fIntervenant, fMinAge].forEach(el => el.addEventListener('input', render));
[fOnlyMine, fOnlyPc].forEach(el => el.addEventListener('change', render)); fIntervenant.addEventListener('change', render);
selAll.addEventListener('change', () => { selAll.addEventListener('change', () => {
tbody.querySelectorAll('.row-cb').forEach(cb => { tbody.querySelectorAll('.row-cb').forEach(cb => {