- 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>
185 lines
11 KiB
HTML
185 lines
11 KiB
HTML
{% extends 'base.html' %}
|
|
{% block title %}Administration — Applications{% endblock %}
|
|
{% block content %}
|
|
<div class="flex justify-between items-center mb-4">
|
|
<div>
|
|
<h2 class="text-xl font-bold text-cyber-accent">Applications (Solutions applicatives)</h2>
|
|
<p class="text-xs text-gray-500 mt-1">Catalogue des solutions applicatives. Synchronisé bidirectionnellement avec iTop.</p>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<a href="/admin/applications/multi-app" class="btn-sm bg-cyber-border text-cyber-accent px-3 py-2">Serveurs multi-app</a>
|
|
{% if can_edit %}
|
|
<button onclick="openAdd()" class="btn-primary px-4 py-2 text-sm">+ Nouvelle application</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
{% if msg %}
|
|
<div class="mb-3 p-2 rounded text-sm {% if 'forbidden' in msg or 'notfound' in msg or 'exists' in msg or 'itop_ko' in msg %}bg-red-900/30 text-cyber-red{% else %}bg-green-900/30 text-cyber-green{% endif %}">
|
|
{% if msg == 'added' %}Application créée.
|
|
{% elif msg == 'exists' %}Cette application existe déjà (nom court).
|
|
{% elif msg.startswith('edited') %}Application modifiée{% if 'itop_ok' in msg %} et poussée vers iTop{% elif 'itop_ko' in msg %} (push iTop échoué){% endif %}.
|
|
{% elif msg.startswith('deleted') %}Application supprimée (dissociée de {{ msg.split('_')[1] if '_' in msg else '?' }} serveurs){% if 'itop_ok' in msg %} + iTop{% elif 'itop_ko' in msg %} — push iTop KO{% endif %}.
|
|
{% elif msg == 'forbidden' %}Permission refusée.
|
|
{% elif msg == 'notfound' %}Application introuvable.
|
|
{% else %}{{ msg }}{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- KPIs -->
|
|
<div class="flex gap-2 mb-4">
|
|
<div class="card p-3 text-center" style="flex:1"><div class="text-2xl font-bold text-cyber-accent">{{ stats.total }}</div><div class="text-xs text-gray-500">Total applications</div></div>
|
|
<div class="card p-3 text-center" style="flex:1"><div class="text-2xl font-bold text-cyber-blue">{{ stats.from_itop }}</div><div class="text-xs text-gray-500">Liées iTop</div></div>
|
|
<div class="card p-3 text-center" style="flex:1"><div class="text-2xl font-bold text-cyber-green">{{ stats.used }}</div><div class="text-xs text-gray-500">Utilisées (avec serveurs)</div></div>
|
|
<div class="card p-3 text-center" style="flex:1"><div class="text-2xl font-bold text-cyber-yellow">{{ stats.total - stats.used }}</div><div class="text-xs text-gray-500">Non utilisées</div></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="Nom / description..." class="text-xs py-1 px-2" style="width:250px">
|
|
<select name="criticite" class="text-xs py-1 px-2" style="width:140px">
|
|
<option value="">Toutes criticités</option>
|
|
{% for v,l in crit_choices %}<option value="{{ v }}" {% if filters.criticite == v %}selected{% endif %}>{{ l }}</option>{% endfor %}
|
|
</select>
|
|
<select name="status" class="text-xs py-1 px-2" style="width:140px">
|
|
<option value="">Tous statuts</option>
|
|
{% for v,l in status_choices %}<option value="{{ v }}" {% if filters.status == v %}selected{% endif %}>{{ l }}</option>{% endfor %}
|
|
</select>
|
|
<select name="has_itop" class="text-xs py-1 px-2" style="width:140px">
|
|
<option value="">Toutes</option>
|
|
<option value="yes" {% if filters.has_itop == 'yes' %}selected{% endif %}>Liées iTop</option>
|
|
<option value="no" {% if filters.has_itop == 'no' %}selected{% endif %}>Locales uniquement</option>
|
|
</select>
|
|
<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>
|
|
<button type="submit" class="btn-primary px-3 py-1 text-xs">Filtrer</button>
|
|
<a href="/admin/applications" class="text-xs text-gray-500 hover:text-cyber-accent">Reset</a>
|
|
<span class="text-xs text-gray-500 ml-auto">{{ apps|length }} apps</span>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Tableau -->
|
|
<div class="card overflow-x-auto">
|
|
<table class="w-full table-cyber text-xs">
|
|
<thead><tr>
|
|
<th class="p-2 text-left">Nom court</th>
|
|
<th class="p-2 text-left">Nom complet</th>
|
|
<th class="p-2">Criticité</th>
|
|
<th class="p-2">Statut</th>
|
|
<th class="p-2">iTop ID</th>
|
|
<th class="p-2 text-left">Domaine(s)</th>
|
|
<th class="p-2">Serveurs liés</th>
|
|
{% if can_edit %}<th class="p-2">Actions</th>{% endif %}
|
|
</tr></thead>
|
|
<tbody>
|
|
{% for a in apps %}
|
|
<tr class="border-t border-cyber-border/30">
|
|
<td class="p-2 font-mono text-cyber-accent">{{ a.nom_court }}</td>
|
|
<td class="p-2 text-gray-300" title="{{ a.description or '' }}">{{ (a.nom_complet or '-')[:60] }}</td>
|
|
<td class="p-2 text-center">
|
|
<span class="badge {% if a.criticite == 'critique' %}badge-red{% elif a.criticite == 'haute' %}badge-yellow{% elif a.criticite == 'standard' %}badge-blue{% else %}badge-gray{% endif %}">{{ a.criticite }}</span>
|
|
</td>
|
|
<td class="p-2 text-center">
|
|
<span class="badge {% if a.status == 'active' %}badge-green{% elif a.status == 'obsolete' %}badge-red{% else %}badge-gray{% endif %}">{{ a.status }}</span>
|
|
</td>
|
|
<td class="p-2 text-center text-gray-500">{{ a.itop_id or '—' }}</td>
|
|
<td class="p-2 text-xs text-gray-300" style="max-width:200px" title="{{ a.domains or '' }}">{{ (a.domains or '—')[:50] }}</td>
|
|
<td class="p-2 text-center">
|
|
{% if a.nb_servers %}<a href="/servers?application_id={{ a.id }}" class="text-cyber-accent hover:underline">{{ a.nb_servers }}</a>{% else %}<span class="text-gray-600">0</span>{% endif %}
|
|
</td>
|
|
{% if can_edit %}
|
|
<td class="p-2 text-center">
|
|
<a href="/admin/applications/{{ a.id }}/assign" class="btn-sm bg-cyber-blue text-black" style="padding:2px 8px;text-decoration:none">+ Serveurs</a>
|
|
<button onclick='openEdit({{ a.id }}, {{ a.nom_court|tojson }}, {{ a.nom_complet|tojson }}, {{ (a.description or "")|tojson }}, {{ (a.editeur or "")|tojson }}, "{{ a.criticite }}", "{{ a.status }}")' class="btn-sm bg-cyber-border text-cyber-accent">Éditer</button>
|
|
<form method="POST" action="/admin/applications/{{ a.id }}/delete" style="display:inline" onsubmit="return confirm('Supprimer {{ a.nom_court }} ? {% if a.nb_servers %}{{ a.nb_servers }} serveurs seront dissociés.{% endif %}{% if a.itop_id %} Également supprimée dans iTop.{% endif %}')">
|
|
<button class="btn-sm bg-red-900/30 text-cyber-red">Suppr.</button>
|
|
</form>
|
|
</td>
|
|
{% endif %}
|
|
</tr>
|
|
{% endfor %}
|
|
{% if not apps %}
|
|
<tr><td colspan="8" class="p-6 text-center text-gray-500">Aucune application</td></tr>
|
|
{% endif %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Modal Add/Edit -->
|
|
{% if can_edit %}
|
|
<div id="app-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:9999;justify-content:center;align-items:center">
|
|
<div class="card p-5" style="width:500px;max-width:95vw">
|
|
<h3 class="text-sm font-bold text-cyber-accent mb-3" id="modal-title">Nouvelle application</h3>
|
|
<form id="app-form" method="POST" action="/admin/applications/add" class="space-y-3">
|
|
<div>
|
|
<label class="text-xs text-gray-500 block mb-1">Nom court *</label>
|
|
<input type="text" name="nom_court" id="f-nom-court" required maxlength="50" class="w-full text-xs">
|
|
</div>
|
|
<div>
|
|
<label class="text-xs text-gray-500 block mb-1">Nom complet</label>
|
|
<input type="text" name="nom_complet" id="f-nom-complet" maxlength="200" class="w-full text-xs">
|
|
</div>
|
|
<div>
|
|
<label class="text-xs text-gray-500 block mb-1">Description</label>
|
|
<textarea name="description" id="f-description" rows="2" maxlength="500" class="w-full text-xs"></textarea>
|
|
</div>
|
|
<div>
|
|
<label class="text-xs text-gray-500 block mb-1">Éditeur</label>
|
|
<input type="text" name="editeur" id="f-editeur" maxlength="100" class="w-full text-xs">
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label class="text-xs text-gray-500 block mb-1">Criticité</label>
|
|
<select name="criticite" id="f-criticite" class="w-full text-xs">
|
|
{% for v,l in crit_choices %}<option value="{{ v }}">{{ l }}</option>{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="text-xs text-gray-500 block mb-1">Statut</label>
|
|
<select name="status" id="f-status" class="w-full text-xs">
|
|
{% for v,l in status_choices %}<option value="{{ v }}">{{ l }}</option>{% endfor %}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div id="push-zone">
|
|
<label class="text-xs text-gray-400 flex items-center gap-2">
|
|
<input type="checkbox" name="push_itop" checked> Créer aussi dans iTop (recommandé)
|
|
</label>
|
|
</div>
|
|
<div class="flex gap-2 justify-end pt-2">
|
|
<button type="button" onclick="document.getElementById('app-modal').style.display='none'" class="btn-sm bg-cyber-border text-gray-300 px-4 py-2">Annuler</button>
|
|
<button type="submit" class="btn-primary px-4 py-2 text-sm">Enregistrer</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function openAdd() {
|
|
document.getElementById('modal-title').textContent = 'Nouvelle application';
|
|
const form = document.getElementById('app-form');
|
|
form.action = '/admin/applications/add';
|
|
form.reset();
|
|
document.getElementById('push-zone').style.display = 'block';
|
|
document.getElementById('app-modal').style.display = 'flex';
|
|
}
|
|
function openEdit(id, nomCourt, nomComplet, description, editeur, criticite, status) {
|
|
document.getElementById('modal-title').textContent = 'Éditer : ' + nomCourt;
|
|
const form = document.getElementById('app-form');
|
|
form.action = '/admin/applications/' + id + '/edit';
|
|
document.getElementById('f-nom-court').value = nomCourt || '';
|
|
document.getElementById('f-nom-complet').value = nomComplet || '';
|
|
document.getElementById('f-description').value = description || '';
|
|
document.getElementById('f-editeur').value = editeur || '';
|
|
document.getElementById('f-criticite').value = criticite || 'basse';
|
|
document.getElementById('f-status').value = status || 'active';
|
|
document.getElementById('push-zone').style.display = 'none'; // pas de checkbox en édit
|
|
document.getElementById('app-modal').style.display = 'flex';
|
|
}
|
|
</script>
|
|
{% endif %}
|
|
{% endblock %}
|