- Admin applications: CRUD module (list/add/edit/delete/assign/multi-app) avec push iTop bidirectionnel (applications.py + 3 templates) - Correspondance prod<->hors-prod: migration vers server_correspondance globale, suppression ancien code quickwin, ajout filtre environnement et solution applicative, colonne environnement dans builder - Servers page: colonne application_name + equivalent(s) via get_links_bulk, filtre application_id, push iTop sur changement application - Patching: bulk_update_application, bulk_update_excludes, validations - Fix paramiko sftp.put (remote_path -> positional arg) - Tools: wiki_to_pdf.py (DokuWiki -> PDF) + generate_ppt.py (PPTX 19 slides DSI patching) + contenu source (processus_patching.txt, script_presentation.txt) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
127 lines
6.3 KiB
HTML
127 lines
6.3 KiB
HTML
{% extends 'base.html' %}
|
|
{% block title %}Associer serveurs — {{ app.nom_court }}{% endblock %}
|
|
{% block content %}
|
|
<div class="flex justify-between items-center mb-4">
|
|
<div>
|
|
<a href="/admin/applications" class="text-xs text-gray-500 hover:text-gray-300">← Applications</a>
|
|
<h2 class="text-xl font-bold text-cyber-accent">Associer serveurs à : <span class="font-mono">{{ app.nom_court }}</span></h2>
|
|
<p class="text-xs text-gray-500 mt-1">Sélectionner des serveurs et les lier à cette application. Push iTop automatique.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filtres -->
|
|
<div class="card p-3 mb-4">
|
|
<form method="GET" class="flex gap-2 items-center flex-wrap">
|
|
<input type="text" name="search" value="{{ filters.search }}" placeholder="Hostname..." class="text-xs py-1 px-2" style="width:200px">
|
|
<select name="domain" class="text-xs py-1 px-2" style="width:160px">
|
|
<option value="">Tous domaines</option>
|
|
{% for d in domains_list %}<option value="{{ d }}" {% if filters.domain == d %}selected{% endif %}>{{ d }}</option>{% endfor %}
|
|
</select>
|
|
<select name="env" class="text-xs py-1 px-2" style="width:140px">
|
|
<option value="">Tous envs</option>
|
|
{% for e in envs_list %}<option value="{{ e }}" {% if filters.env == e %}selected{% endif %}>{{ e }}</option>{% endfor %}
|
|
</select>
|
|
<select name="assigned" class="text-xs py-1 px-2" style="width:200px">
|
|
<option value="">Tous serveurs</option>
|
|
<option value="none" {% if filters.assigned == 'none' %}selected{% endif %}>Sans app</option>
|
|
<option value="other" {% if filters.assigned == 'other' %}selected{% endif %}>Liés à autre app</option>
|
|
<option value="current" {% if filters.assigned == 'current' %}selected{% endif %}>Déjà liés à celle-ci</option>
|
|
</select>
|
|
<button type="submit" class="btn-primary px-3 py-1 text-xs">Filtrer</button>
|
|
<a href="/admin/applications/{{ app.id }}/assign" class="text-xs text-gray-500 hover:text-cyber-accent">Reset</a>
|
|
<span class="text-xs text-gray-500 ml-auto">{{ total }} serveurs</span>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Bulk bar -->
|
|
<div id="bulk-bar" class="card p-3 mb-2 flex gap-2 items-center flex-wrap" style="display:none;border-left:3px solid #00d4ff">
|
|
<span class="text-xs text-gray-400"><b id="bulk-count">0</b> sélectionné(s)</span>
|
|
<button onclick="assignSelected()" class="btn-primary px-4 py-2 text-sm">Associer à <span class="font-mono">{{ app.nom_court }}</span></button>
|
|
<span id="bulk-result" class="text-xs ml-2"></span>
|
|
</div>
|
|
|
|
<!-- Tableau -->
|
|
<div class="card overflow-x-auto">
|
|
<table class="w-full table-cyber text-xs">
|
|
<thead><tr>
|
|
<th class="p-2 w-8"><input type="checkbox" id="check-all" onchange="toggleAll(this)"></th>
|
|
<th class="p-2 text-left">Hostname</th>
|
|
<th class="p-2">OS</th>
|
|
<th class="p-2">Domaine</th>
|
|
<th class="p-2">Env</th>
|
|
<th class="p-2 text-left">App actuelle</th>
|
|
</tr></thead>
|
|
<tbody>
|
|
{% for s in servers %}
|
|
<tr class="border-t border-cyber-border/30 {% if s.application_id == app.id %}bg-green-900/10{% endif %}">
|
|
<td class="p-2 text-center">
|
|
<input type="checkbox" class="srv-check" value="{{ s.id }}" onchange="updateBulk()" {% if s.application_id == app.id %}disabled title="Déjà lié"{% endif %}>
|
|
</td>
|
|
<td class="p-2 font-mono text-cyber-accent">{{ s.hostname }}</td>
|
|
<td class="p-2 text-center">{{ s.os_family or '-' }}</td>
|
|
<td class="p-2 text-center text-gray-400">{{ s.domain_name or '-' }}</td>
|
|
<td class="p-2 text-center">{{ s.env_name or '-' }}</td>
|
|
<td class="p-2 text-xs">
|
|
{% if s.application_id == app.id %}
|
|
<span class="text-cyber-green">✓ déjà lié</span>
|
|
{% elif s.application_name %}
|
|
<span class="text-cyber-yellow" title="Sera remplacée">{{ s.application_name }}</span>
|
|
{% else %}
|
|
<span class="text-gray-600">—</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
{% if not servers %}
|
|
<tr><td colspan="6" class="p-6 text-center text-gray-500">Aucun serveur</td></tr>
|
|
{% endif %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
{% if total_pages > 1 %}
|
|
<div class="flex justify-center gap-2 mt-4">
|
|
{% for p in range(1, total_pages + 1) %}
|
|
<a href="?page={{ p }}{% for k,v in filters.items() %}{% if v %}&{{ k }}={{ v }}{% endif %}{% endfor %}"
|
|
class="btn-sm {% if p == page %}bg-cyber-accent text-black{% else %}bg-cyber-border text-gray-300{% endif %} px-2 py-1">{{ p }}</a>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<script>
|
|
function toggleAll(cb) {
|
|
document.querySelectorAll('.srv-check:not(:disabled)').forEach(c => c.checked = cb.checked);
|
|
updateBulk();
|
|
}
|
|
function updateBulk() {
|
|
const ids = Array.from(document.querySelectorAll('.srv-check:checked')).map(c => parseInt(c.value));
|
|
window._selectedIds = ids;
|
|
document.getElementById('bulk-count').textContent = ids.length;
|
|
document.getElementById('bulk-bar').style.display = ids.length > 0 ? 'flex' : 'none';
|
|
}
|
|
function assignSelected() {
|
|
const ids = window._selectedIds || [];
|
|
if (!ids.length) return;
|
|
if (!confirm('Associer ' + ids.length + ' serveur(s) à "{{ app.nom_court }}" ?\n(Cela écrasera leur app actuelle + push iTop)')) return;
|
|
const res = document.getElementById('bulk-result');
|
|
res.textContent = 'En cours...'; res.className = 'text-xs ml-2 text-gray-400';
|
|
fetch('/admin/applications/{{ app.id }}/assign', {
|
|
method: 'POST', credentials: 'same-origin',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({server_ids: ids})
|
|
})
|
|
.then(r => r.json())
|
|
.then(d => {
|
|
if (d.ok) {
|
|
res.innerHTML = '✓ ' + d.updated + ' associés — iTop: <b class="text-cyber-green">' + d.itop_pushed + '</b> OK / <b class="text-cyber-red">' + d.itop_errors + '</b> KO';
|
|
res.className = 'text-xs ml-2';
|
|
setTimeout(() => location.reload(), 1500);
|
|
} else {
|
|
res.textContent = '✗ ' + (d.msg || 'Erreur'); res.className = 'text-xs ml-2 text-cyber-red';
|
|
}
|
|
});
|
|
}
|
|
</script>
|
|
{% endblock %}
|