feat(snapshots): dropdown Intervenant alimente depuis users actifs (hors admin) au pageload

- Router: charge la liste des users (is_active=true AND role <> 'admin')
  depuis la table users + passe au contexte
- Template: dropdown rempli en Jinja avec username + (toi) si current user + display_name
  si different. Si user connecte non present en BDD (cas rare), ajoute une entree
  en bonus
- JS: supprime rebuildIntervenantDropdown (la liste reste figee, plus simple,
  predictible). Note conservee pour explication.
This commit is contained in:
Pierre & Lumière 2026-05-07 21:04:00 +02:00
parent 46b80474c2
commit cefddd2ea0
2 changed files with 18 additions and 26 deletions

View File

@ -49,12 +49,19 @@ async def snapshots_page(request: Request, db=Depends(get_db)):
vcenters = db.execute(text( vcenters = db.execute(text(
"SELECT id, name, endpoint FROM vcenters WHERE is_active = true ORDER BY name" "SELECT id, name, endpoint FROM vcenters WHERE is_active = true ORDER BY name"
)).fetchall() )).fetchall()
# Liste des intervenants disponibles : users actifs non-admin (cf table users)
intervenants = db.execute(text("""
SELECT username, display_name FROM users
WHERE is_active = true AND role <> 'admin'
ORDER BY username
""")).fetchall()
ctx = base_context(request, db, user) ctx = base_context(request, db, user)
ctx.update({ ctx.update({
"app_name": APP_NAME, "app_name": APP_NAME,
"intervenant_default": intervenant, "intervenant_default": intervenant,
"vcenters": vcenters, "vcenters": vcenters,
"intervenants_list": intervenants,
"can_delete": _can_delete_snaps(perms), "can_delete": _can_delete_snaps(perms),
}) })
return templates.TemplateResponse("snapshots.html", ctx) return templates.TemplateResponse("snapshots.html", ctx)

View File

@ -58,9 +58,14 @@
<div class="col-span-3"> <div class="col-span-3">
<label class="text-xs text-gray-500">Intervenant</label> <label class="text-xs text-gray-500">Intervenant</label>
<select id="f-intervenant" class="w-full"> <select id="f-intervenant" class="w-full">
<option value="{{ intervenant_default }}" selected>{{ intervenant_default or '(toi)' }}</option> {% for u in intervenants_list %}
<option value="{{ u.username }}"{% if u.username == intervenant_default %} selected{% endif %}>{{ u.username }}{% if u.username == intervenant_default %} (toi){% endif %}{% if u.display_name and u.display_name != u.username %} — {{ u.display_name }}{% endif %}</option>
{% endfor %}
{% if intervenant_default and intervenant_default not in intervenants_list|map(attribute='username')|list %}
<option value="{{ intervenant_default }}" selected>{{ intervenant_default }} (toi, hors liste)</option>
{% endif %}
</select> </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> <p class="text-xs text-gray-500 mt-1">Users actifs (hors admins) — défaut = toi.</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>
@ -150,29 +155,10 @@
}); });
} }
function rebuildIntervenantDropdown() { // Note: la liste des intervenants est désormais figée au pageload (cf template,
// Liste des auteurs distincts des snapshots PatchCenter trouvés // alimentée depuis la table users actifs hors admin). On ne la reconstruit plus
const authors = Array.from(new Set( // dynamiquement après scan vCenter — un user qui aurait fait un snapshot mais
allSnaps.filter(s => s.origin === 'patchcenter' && s.author).map(s => s.author) // serait inactif/admin ne sera pas filtrable nominativement, tant pis pour ce cas.
)).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)
@ -296,7 +282,6 @@
(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>';