patchcenter/app/templates/admin_applications_assign.html
Admin MPCZ 677f621c81 Admin applications + correspondance cleanup + tools presentation DSI
- 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>
2026-04-13 21:11:58 +02:00

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">&larr; 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 %}