patchcenter/app/templates/partials/qualys_asset_detail.html
Khalid MOUTAOUAKIL 8e62b1fb11 Qualys complet, contacts, audit refactoré, bulk serveurs
Qualys:
- Recherche API temps réel + cache 24h base locale
- Tags: liste DYN/STAT, mapping V3 (DOM-*, TYP-*, APP-*), nb assets cliquable
- CRUD tags: créer STAT, supprimer, resync API
- Détail asset: infos + décodage nomenclature V3 + tags assignés
- Ajout/retrait tag unitaire avec autocomplete filtrable
- Bulk add/remove tag en masse avec dropdown filtrable
- Tags retirer: charge dynamiquement les STAT assignés aux assets sélectionnés
- Resync assets sélectionnés + retour même recherche

Contacts:
- 50 contacts importés avec 93 scopes (domaine/app/serveur/zone par env)
- 13 rôles (responsable_domaine, ra_prod, ra_recette, referent_technique...)
- Recherche par nom/email/serveur (affiche contacts liés)
- CRUD complet: éditer, scopes, activer/désactiver, supprimer
- Serveurs liés calculés dynamiquement depuis les scopes

Audit:
- Restructuré: Audit général + sous-menu Spécifique
- Dernier audit global affiché avec date
- Lancer audit général avec exclusions (domaines/zones) et parallélisme
- KPIs Qualys KO et S1 KO cliquables
- Export CSV

Serveurs:
- Actions groupées bulk (domaine, env, tier, état, owner, licence)
- Dashboard: KPI EOL ajouté
- Filtre état: EOL + en décommissionnement ajoutés
- 138 serveurs EOL importés depuis Qualys (owner=na, hors périmètre)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 00:47:26 +02:00

107 lines
6.5 KiB
HTML

<div>
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-bold text-cyber-accent">{{ a.name or a.hostname }}</h3>
<button onclick="document.getElementById('qualys-detail').style.display='none'" class="text-gray-500 hover:text-white text-xl">&times;</button>
</div>
<div class="grid grid-cols-2 gap-4">
<!-- Infos -->
<div>
<h4 class="text-xs text-cyber-accent font-bold uppercase mb-2 border-b border-cyber-border pb-1">Informations</h4>
<div class="space-y-1 text-xs">
<div><span class="text-gray-500">Hostname:</span> <span class="font-mono">{{ a.hostname }}</span></div>
<div><span class="text-gray-500">IP:</span> <span class="font-mono text-cyber-green">{{ a.ip_address or '-' }}</span></div>
<div><span class="text-gray-500">FQDN:</span> <span class="font-mono">{{ a.fqdn or '-' }}</span></div>
<div><span class="text-gray-500">OS:</span> {{ a.os or '-' }}</div>
<div><span class="text-gray-500">Agent:</span> <span class="badge {% if a.agent_status and 'ACTIVE' in a.agent_status %}badge-green{% else %}badge-red{% endif %}">{{ a.agent_status or '-' }}</span></div>
<div><span class="text-gray-500">Version:</span> {{ a.agent_version or '-' }}</div>
<div><span class="text-gray-500">Dernier check-in:</span> {{ a.last_checkin or '-' }}</div>
<div><span class="text-gray-500">Qualys ID:</span> {{ a.qualys_asset_id }}</div>
</div>
</div>
<!-- Décodage -->
<div>
<h4 class="text-xs text-cyber-accent font-bold uppercase mb-2 border-b border-cyber-border pb-1">Décodage nomenclature</h4>
<div class="space-y-1 text-xs">
<div><span class="text-gray-500">Type:</span> {{ decoded.type }}</div>
<div><span class="text-gray-500">Environnement:</span> {{ decoded.env }}</div>
<div><span class="text-gray-500">Domaine:</span> {{ decoded.domain }}</div>
</div>
<h4 class="text-xs text-cyber-accent font-bold uppercase mt-3 mb-1">Tags suggérés</h4>
<div class="flex flex-wrap gap-1">
{% for t in decoded.tags %}
{% set found = false %}
{% for ct in tags %}{% if ct.name == t %}{% set found = true %}{% endif %}{% endfor %}
<span class="badge {% if found %}badge-green{% else %}badge-red{% endif %}" title="{% if found %}Assigné{% else %}Manquant{% endif %}">{{ t }}</span>
{% endfor %}
</div>
</div>
</div>
<!-- Tags assignés -->
<div class="mt-4">
<h4 class="text-xs text-cyber-accent font-bold uppercase mb-2 border-b border-cyber-border pb-1">Tags assignés ({{ tags|length }})</h4>
<div class="space-y-1">
{% for t in tags %}
<div class="flex items-center gap-2 text-xs">
<span class="badge {% if t.is_dynamic %}badge-blue{% else %}badge-yellow{% endif %}">{{ 'DYN' if t.is_dynamic else 'STAT' }}</span>
<span class="font-mono text-cyber-accent">{{ t.name }}</span>
{% if not t.is_dynamic %}
<form method="POST" action="/qualys/asset/{{ a.qualys_asset_id }}/tag/remove" style="display:inline"
hx-post="/qualys/asset/{{ a.qualys_asset_id }}/tag/remove" hx-target="#tag-result" hx-swap="innerHTML">
<input type="hidden" name="tag_id" value="{{ t.qualys_tag_id }}">
<button class="text-gray-600 hover:text-cyber-red" title="Retirer ce tag"></button>
</form>
{% endif %}
</div>
{% endfor %}
{% if not tags %}<span class="text-xs text-gray-500">Aucun tag</span>{% endif %}
</div>
<div id="tag-result" class="mt-1"></div>
</div>
<!-- Ajouter un tag -->
<div class="mt-4">
<h4 class="text-xs text-cyber-accent font-bold uppercase mb-2 border-b border-cyber-border pb-1">Ajouter un tag</h4>
<form hx-post="/qualys/asset/{{ a.qualys_asset_id }}/tag/add" hx-target="#tag-result" hx-swap="innerHTML" class="flex gap-2 items-center">
<input type="hidden" name="tag_id" id="add-tag-id-{{ a.qualys_asset_id }}">
<input type="text" id="add-tag-input-{{ a.qualys_asset_id }}" class="text-xs py-1 px-2 flex-1 font-mono" placeholder="Filtrer et sélectionner un tag..." autocomplete="off"
onkeyup="filterTags(this, '{{ a.qualys_asset_id }}')" onfocus="document.getElementById('tag-dropdown-{{ a.qualys_asset_id }}').style.display='block'">
<button type="submit" class="btn-sm bg-cyber-accent text-black">Ajouter</button>
</form>
<div id="tag-dropdown-{{ a.qualys_asset_id }}" class="bg-cyber-card border border-cyber-border rounded mt-1 overflow-y-auto text-xs" style="display:none; max-height:150px">
{% set assigned_ids = tags | map(attribute='qualys_tag_id') | list %}
{% for t in all_tags %}
{% if t.qualys_tag_id not in assigned_ids %}
<div class="px-2 py-1 hover:bg-cyber-border/30 cursor-pointer tag-option" data-id="{{ t.qualys_tag_id }}" data-name="{{ t.name }}"
onclick="selectTag('{{ a.qualys_asset_id }}', '{{ t.qualys_tag_id }}', '{{ t.name }}')">
<span class="font-mono">{{ t.name }}</span>
<span class="badge {% if t.is_dynamic %}badge-blue{% else %}badge-yellow{% endif %} text-[8px] ml-1">{{ 'DYN' if t.is_dynamic else 'STAT' }}</span>
</div>
{% endif %}
{% endfor %}
</div>
</div>
<script>
function filterTags(input, aid) {
var q = input.value.toLowerCase();
var dd = document.getElementById('tag-dropdown-' + aid);
dd.style.display = 'block';
dd.querySelectorAll('.tag-option').forEach(function(el) {
el.style.display = el.dataset.name.toLowerCase().includes(q) ? '' : 'none';
});
}
function selectTag(aid, tid, tname) {
document.getElementById('add-tag-id-' + aid).value = tid;
document.getElementById('add-tag-input-' + aid).value = tname;
document.getElementById('tag-dropdown-' + aid).style.display = 'none';
}
document.addEventListener('click', function(e) {
document.querySelectorAll('[id^=tag-dropdown-]').forEach(function(dd) {
if (!dd.contains(e.target) && !e.target.id.startsWith('add-tag-input-')) dd.style.display = 'none';
});
});
</script>
</div>