204 lines
14 KiB
HTML
204 lines
14 KiB
HTML
{% extends 'base.html' %}
|
|
{% block title %}Serveurs{% endblock %}
|
|
|
|
{% macro sort_url(col) -%}
|
|
?sort={{ col }}&sort_dir={% if sort == col and sort_dir == 'asc' %}desc{% else %}asc{% endif %}&search={{ filters.search or '' }}&domain={{ filters.domain or '' }}&env={{ filters.env or '' }}&tier={{ filters.tier or '' }}&etat={{ filters.etat or '' }}&os={{ filters.os or '' }}&owner={{ filters.owner or '' }}&page=1
|
|
{%- endmacro %}
|
|
|
|
{% macro sort_icon(col) -%}
|
|
{% if sort == col %}{% if sort_dir == 'asc' %}▲{% else %}▼{% endif %}{% endif %}
|
|
{%- endmacro %}
|
|
|
|
{% macro qs(p) -%}
|
|
?page={{ p }}&search={{ filters.search or '' }}&domain={{ filters.domain or '' }}&env={{ filters.env or '' }}&tier={{ filters.tier or '' }}&etat={{ filters.etat or '' }}&os={{ filters.os or '' }}&owner={{ filters.owner or '' }}&sort={{ sort }}&sort_dir={{ sort_dir }}
|
|
{%- endmacro %}
|
|
|
|
{% block content %}
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h2 class="text-xl font-bold text-cyber-accent">Serveurs <span class="text-sm text-gray-500">({{ total }})</span></h2>
|
|
<div class="flex gap-2">
|
|
<a href="/servers/export-csv?search={{ filters.search or '' }}&domain={{ filters.domain or '' }}&env={{ filters.env or '' }}&tier={{ filters.tier or '' }}&etat={{ filters.etat or '' }}&os={{ filters.os or '' }}&owner={{ filters.owner or '' }}" class="btn-sm bg-cyber-green text-black">Export CSV</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filtres -->
|
|
<form method="GET" class="card p-3 mb-4 flex gap-3 items-center flex-wrap">
|
|
<input type="hidden" name="sort" value="{{ sort }}">
|
|
<input type="hidden" name="sort_dir" value="{{ sort_dir }}">
|
|
<input type="text" name="search" value="{{ filters.search or '' }}" placeholder="Rechercher..." class="w-44" hx-get="/servers" hx-trigger="keyup changed delay:300ms" hx-target="#server-table" hx-select="#server-table" hx-push-url="true">
|
|
<select name="domain" onchange="this.form.submit()"><option value="">Domaine</option>
|
|
{% for d in domains_list %}<option value="{{ d.code }}" {% if filters.domain == d.code %}selected{% endif %}>{{ d.name }}</option>{% endfor %}
|
|
</select>
|
|
<select name="env" onchange="this.form.submit()"><option value="">Env</option>
|
|
{% for e in ['Développement','Intégration','Pré-Prod','Production','Recette','Test','Formation'] %}<option value="{{ e }}" {% if filters.env == e %}selected{% endif %}>{{ e }}</option>{% endfor %}
|
|
<option value="__null__" {% if filters.env == '__null__' %}selected{% endif %}>(Sans env)</option>
|
|
</select>
|
|
<select name="tier" onchange="this.form.submit()"><option value="">Tier</option>
|
|
{% for t in ['tier0','tier1','tier2','tier3'] %}<option value="{{ t }}" {% if filters.tier == t %}selected{% endif %}>{{ t }}</option>{% endfor %}
|
|
</select>
|
|
<select name="etat" onchange="this.form.submit()"><option value="">Etat</option>
|
|
{% for e in ['Production','Implémentation','Stock','Obsolète','prêt','tests'] %}<option value="{{ e }}" {% if filters.etat == e %}selected{% endif %}>{{ e }}</option>{% endfor %}
|
|
<option value="__null__" {% if filters.etat == '__null__' %}selected{% endif %}>(Sans état)</option>
|
|
</select>
|
|
<select name="os" onchange="this.form.submit()"><option value="">OS</option>
|
|
<option value="linux" {% if filters.os == 'linux' %}selected{% endif %}>Linux</option>
|
|
<option value="windows" {% if filters.os == 'windows' %}selected{% endif %}>Windows</option>
|
|
</select>
|
|
<select name="zone" onchange="this.form.submit()"><option value="">Zone</option>
|
|
{% for z in zones_list %}<option value="{{ z.name }}" {% if filters.zone == z.name %}selected{% endif %}>{{ z.name }}{% if z.is_dmz %} (DMZ){% endif %}</option>{% endfor %}
|
|
<option value="__null__" {% if filters.zone == '__null__' %}selected{% endif %}>(Sans zone)</option>
|
|
</select>
|
|
<select name="owner" onchange="this.form.submit()"><option value="">Owner</option>
|
|
<option value="secops" {% if filters.owner == 'secops' %}selected{% endif %}>secops</option>
|
|
<option value="ipop" {% if filters.owner == 'ipop' %}selected{% endif %}>ipop</option>
|
|
<option value="na" {% if filters.owner == 'na' %}selected{% endif %}>na</option>
|
|
</select>
|
|
<select name="licence" onchange="this.form.submit()"><option value="">Licence</option>
|
|
<option value="active" {% if filters.licence == 'active' %}selected{% endif %}>active</option>
|
|
<option value="obsolete" {% if filters.licence == 'obsolete' %}selected{% endif %}>obsolete (EOL)</option>
|
|
<option value="els" {% if filters.licence == 'els' %}selected{% endif %}>els</option>
|
|
<option value="__null__" {% if filters.licence == '__null__' %}selected{% endif %}>(Sans licence)</option>
|
|
</select>
|
|
<select name="application" onchange="this.form.submit()" style="max-width:200px"><option value="">Solution app.</option>
|
|
{% for a in applications_list %}<option value="{{ a.application_name }}" {% if filters.application == a.application_name %}selected{% endif %}>{{ a.application_name }} ({{ a.c }})</option>{% endfor %}
|
|
</select>
|
|
<button type="submit" class="btn-primary px-3 py-1 text-sm">Filtrer</button>
|
|
<a href="/servers" class="text-xs text-gray-500 hover:text-gray-300">Reset</a>
|
|
</form>
|
|
|
|
{% if can_edit_servers %}
|
|
<!-- Actions groupées -->
|
|
<div id="bulk-bar" class="card p-3 mb-2 flex gap-3 items-center flex-wrap" style="display:none">
|
|
<span class="text-xs text-gray-400" id="bulk-count">0 sélectionné(s)</span>
|
|
<form method="POST" action="/servers/bulk" id="bulk-form" class="flex gap-2 items-center flex-wrap">
|
|
<input type="hidden" name="server_ids" id="bulk-ids">
|
|
<select name="bulk_field" class="text-xs py-1 px-2" id="bulk-field">
|
|
<option value="">— Action —</option>
|
|
<option value="domain_code">Domaine</option>
|
|
<option value="env_code">Environnement</option>
|
|
<option value="tier">Tier</option>
|
|
<option value="etat">État</option>
|
|
<option value="patch_os_owner">Owner</option>
|
|
<option value="licence_support">Licence</option>
|
|
</select>
|
|
<select name="bulk_value" class="text-xs py-1 px-2" id="bulk-value">
|
|
<option value="">— Valeur —</option>
|
|
</select>
|
|
<button type="submit" class="btn-primary px-3 py-1 text-xs">Appliquer</button>
|
|
</form>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<script>
|
|
const bulkValues = {
|
|
domain_code: [{% for d in domains_list %}{v:"{{ d.code }}", l:"{{ d.name }}"},{% endfor %}],
|
|
env_code: [{% for e in envs_list %}{v:"{{ e.code }}", l:"{{ e.name }}"},{% endfor %}],
|
|
tier: [{v:"tier0",l:"tier0"},{v:"tier1",l:"tier1"},{v:"tier2",l:"tier2"},{v:"tier3",l:"tier3"}],
|
|
etat: [{v:"Production",l:"Production"},{v:"Implémentation",l:"Implémentation"},{v:"Stock",l:"Stock"},{v:"Obsolète",l:"Obsolète"},{v:"prêt",l:"prêt"},{v:"tests",l:"tests"}],
|
|
patch_os_owner: [{v:"secops",l:"secops"},{v:"ipop",l:"ipop"},{v:"na",l:"na"}],
|
|
licence_support: [{v:"active",l:"active"},{v:"obsolete",l:"obsolete"},{v:"els",l:"els"}],
|
|
};
|
|
document.getElementById('bulk-field').addEventListener('change', function() {
|
|
const sel = document.getElementById('bulk-value');
|
|
sel.innerHTML = '<option value="">— Valeur —</option>';
|
|
(bulkValues[this.value] || []).forEach(o => { sel.innerHTML += '<option value="'+o.v+'">'+o.l+'</option>'; });
|
|
});
|
|
function updateBulk() {
|
|
const checks = document.querySelectorAll('input[name=srv]:checked');
|
|
const bar = document.getElementById('bulk-bar');
|
|
const count = document.getElementById('bulk-count');
|
|
const ids = document.getElementById('bulk-ids');
|
|
if (checks.length > 0) {
|
|
bar.style.display = 'flex';
|
|
count.textContent = checks.length + ' sélectionné(s)';
|
|
ids.value = Array.from(checks).map(c => c.value).join(',');
|
|
} else { bar.style.display = 'none'; }
|
|
}
|
|
</script>
|
|
|
|
<!-- Table -->
|
|
<div id="server-table" class="card overflow-x-auto">
|
|
<table class="w-full table-cyber">
|
|
<thead><tr>
|
|
{% if can_edit_servers %}<th class="p-2 w-8"><input type="checkbox" id="check-all"></th>{% endif %}
|
|
<th class="text-left p-2"><a href="{{ sort_url('hostname') }}" class="hover:text-cyber-accent">Hostname {{ sort_icon('hostname') }}</a></th>
|
|
<th class="p-2"><a href="{{ sort_url('domaine') }}" class="hover:text-cyber-accent">Domaine {{ sort_icon('domaine') }}</a></th>
|
|
<th class="p-2"><a href="{{ sort_url('env') }}" class="hover:text-cyber-accent">Env {{ sort_icon('env') }}</a></th>
|
|
<th class="p-2"><a href="{{ sort_url('zone') }}" class="hover:text-cyber-accent">Zone {{ sort_icon('zone') }}</a></th>
|
|
<th class="p-2"><a href="{{ sort_url('os') }}" class="hover:text-cyber-accent">OS {{ sort_icon('os') }}</a></th>
|
|
<th class="p-2">Version</th>
|
|
<th class="p-2">Licence</th>
|
|
<th class="p-2"><a href="{{ sort_url('tier') }}" class="hover:text-cyber-accent">Tier {{ sort_icon('tier') }}</a></th>
|
|
<th class="p-2"><a href="{{ sort_url('etat') }}" class="hover:text-cyber-accent">Etat {{ sort_icon('etat') }}</a></th>
|
|
<th class="p-2"><a href="{{ sort_url('owner') }}" class="hover:text-cyber-accent">Owner {{ sort_icon('owner') }}</a></th>
|
|
<th class="p-2 text-left">Solution applicative</th>
|
|
<th class="p-2 text-left">Équivalent(s)</th>
|
|
<th class="p-2">Actions</th>
|
|
</tr></thead>
|
|
<tbody>
|
|
{% for s in servers %}
|
|
<tr id="row-{{ s.id }}" class="group" hx-get="/servers/{{ s.id }}/detail" hx-target="#detail-panel" hx-swap="innerHTML" onclick="openPanel()">
|
|
{% if can_edit_servers %}<td class="p-2" onclick="event.stopPropagation()"><input type="checkbox" name="srv" value="{{ s.id }}" onchange="updateBulk()"></td>{% endif %}
|
|
<td class="p-2 font-mono text-sm text-cyber-accent">{{ s.hostname }}</td>
|
|
<td class="p-2 text-center text-xs">{{ s.domaine or '-' }}</td>
|
|
<td class="p-2 text-center"><span class="badge {% if s.environnement == 'Production' %}badge-green{% elif s.environnement == 'Recette' %}badge-yellow{% else %}badge-gray{% endif %}"title="{{ s.environnement or '' }}">{{ (s.environnement or '-')[:6] }}</span></td>
|
|
<td class="p-2 text-center"><span class="badge {% if s.zone == 'DMZ' %}badge-red{% elif s.zone == 'EMV' %}badge-yellow{% else %}badge-blue{% endif %}">{{ s.zone or 'LAN' }}</span></td>
|
|
<td class="p-2 text-center text-xs">{{ s.os_family or '-' }}</td>
|
|
<td class="p-2 text-center text-xs text-gray-400" title="{{ s.os_version or '' }}">{{ s.os_short or '-' }}</td>
|
|
<td class="p-2 text-center"><span class="badge {% if s.licence_support == 'active' %}badge-green{% elif s.licence_support == 'obsolete' %}badge-red{% elif s.licence_support == 'els' %}badge-yellow{% else %}badge-gray{% endif %}">{{ s.licence_support }}</span></td>
|
|
<td class="p-2 text-center"><span class="badge {% if s.tier == 'tier0' %}badge-red{% elif s.tier == 'tier1' %}badge-yellow{% elif s.tier == 'tier2' %}badge-blue{% else %}badge-green{% endif %}">{{ s.tier }}</span></td>
|
|
<td class="p-2 text-center"><span class="badge {% if s.etat == 'Production' %}badge-green{% elif s.etat in ('Obsolète','EOL') %}badge-red{% else %}badge-yellow{% endif %}"title="{{ s.etat or '' }}">{% if s.etat == 'Obsolète' %}Décom.{% elif s.etat == 'EOL' %}EOL{% elif s.etat == 'Production' %}Prod{% elif s.etat == 'Implémentation' %}Implm{% elif s.etat == 'Stock' %}Stock{% else %}{{ (s.etat or '')[:8] }}{% endif %}</span></td>
|
|
<td class="p-2 text-center text-xs">{{ s.patch_os_owner or '-' }}</td>
|
|
<td class="p-2 text-xs text-gray-300" title="{{ s.application_name or '' }}">{{ (s.application_name or '-')[:35] }}</td>
|
|
<td class="p-2 text-xs" onclick="event.stopPropagation()" style="max-width:220px">
|
|
{% set link = links.get(s.id, {}) %}
|
|
{% if link.as_prod %}
|
|
<span class="text-cyber-green" style="font-size:10px">→ non-prod :</span>
|
|
{% for l in link.as_prod %}<span class="font-mono text-gray-300" title="{{ l.env_name or '' }}">{{ l.hostname }}{% if not loop.last %}, {% endif %}</span>{% endfor %}
|
|
{% elif link.as_nonprod %}
|
|
<span class="text-cyber-yellow" style="font-size:10px">→ prod :</span>
|
|
{% for l in link.as_nonprod %}<span class="font-mono text-gray-300">{{ l.hostname }}{% if not loop.last %}, {% endif %}</span>{% endfor %}
|
|
{% else %}
|
|
<span class="text-gray-600">—</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="p-2 text-center" onclick="event.stopPropagation()">
|
|
{% if can_edit_servers %}
|
|
<button class="btn-sm bg-cyber-border text-cyber-accent" hx-get="/servers/{{ s.id }}/edit" hx-target="#detail-panel" hx-swap="innerHTML" onclick="openPanel()">Edit</button>
|
|
{% else %}
|
|
<span class="text-xs text-gray-600">—</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
<div class="flex justify-between items-center mt-4 text-sm text-gray-500">
|
|
<span>Page {{ page }} / {{ ((total - 1) // per_page) + 1 }} — {{ total }} serveurs</span>
|
|
<div class="flex gap-2">
|
|
{% if page > 1 %}<a href="{{ qs(page - 1) }}" class="btn-sm bg-cyber-border text-gray-300">Precedent</a>{% endif %}
|
|
{% if page * per_page < total %}<a href="{{ qs(page + 1) }}" class="btn-sm bg-cyber-border text-gray-300">Suivant</a>{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function openPanel() {
|
|
document.getElementById("detail-panel").scrollTop = 0;
|
|
document.getElementById('detail-panel').style.width = '400px';
|
|
document.getElementById('detail-panel').style.overflow = 'auto';
|
|
window.scrollTo({top: 0, behavior: 'smooth'});
|
|
}
|
|
function closePanel() {
|
|
document.getElementById('detail-panel').style.width = '0';
|
|
document.getElementById('detail-panel').style.overflow = 'hidden';
|
|
}
|
|
document.getElementById('check-all').addEventListener('change', function(e) {
|
|
document.querySelectorAll('input[name=srv]').forEach(cb => cb.checked = e.target.checked);
|
|
updateBulk();
|
|
});
|
|
</script>
|
|
{% endblock %}
|