Modules: Dashboard, Serveurs, Campagnes, Planning, Specifiques, Settings, Users Stack: FastAPI + Jinja2 + HTMX + Alpine.js + TailwindCSS + PostgreSQL Features: Qualys sync, prereqs auto, planning annuel, server specifics, role-based access Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
251 lines
16 KiB
HTML
251 lines
16 KiB
HTML
{% extends 'base.html' %}
|
|
{% block title %}{{ c.label or c.week_code }}{% endblock %}
|
|
{% block content %}
|
|
|
|
<!-- Header -->
|
|
<div class="flex justify-between items-center mb-4">
|
|
<div>
|
|
<a href="/campaigns" class="text-xs text-gray-500 hover:text-gray-300">← Campagnes</a>
|
|
<h2 class="text-xl font-bold text-cyber-accent">{{ c.label or c.week_code }}</h2>
|
|
<div class="flex items-center gap-3 mt-1">
|
|
<span class="badge {% if c.status == 'draft' %}badge-gray{% elif c.status == 'planned' %}badge-blue{% elif c.status == 'in_progress' %}badge-yellow{% elif c.status == 'completed' %}badge-green{% else %}badge-red{% endif %}">{{ c.status }}</span>
|
|
<span class="text-sm text-gray-500">{{ c.week_code }} {{ c.year }}</span>
|
|
{% if c.date_start %}<span class="text-sm text-gray-500">{{ c.date_start.strftime('%d/%m/%Y') }}{% if c.date_end %} → {{ c.date_end.strftime('%d/%m/%Y') }}{% endif %}</span>{% endif %}
|
|
<span class="text-xs text-gray-600">par {{ c.created_by_name or '-' }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
{% if c.status == 'draft' %}
|
|
{% if can_plan %}
|
|
<form method="POST" action="/campaigns/{{ c.id }}/status"><input type="hidden" name="new_status" value="planned">
|
|
<button class="btn-primary px-4 py-2 text-sm">Planifier</button></form>
|
|
{% else %}
|
|
<button class="btn-sm bg-gray-700 text-gray-500 px-4 py-2 cursor-not-allowed" title="Tous les prereqs doivent etre valides">Planifier (prereqs requis)</button>
|
|
{% endif %}
|
|
{% elif c.status == 'planned' %}
|
|
<form method="POST" action="/campaigns/{{ c.id }}/status"><input type="hidden" name="new_status" value="in_progress">
|
|
<button class="btn-primary px-4 py-2 text-sm">Demarrer</button></form>
|
|
{% elif c.status == 'in_progress' %}
|
|
<form method="POST" action="/campaigns/{{ c.id }}/status"><input type="hidden" name="new_status" value="completed">
|
|
<button class="btn-sm bg-cyber-green text-black px-4 py-2">Terminer</button></form>
|
|
{% endif %}
|
|
{% if c.status in ('draft', 'planned') %}
|
|
<form method="POST" action="/campaigns/{{ c.id }}/status"><input type="hidden" name="new_status" value="cancelled">
|
|
<button class="btn-sm bg-red-900/30 text-cyber-red px-4 py-2" onclick="return confirm('Annuler ?')">Annuler</button></form>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
{% if msg %}
|
|
<div class="mb-3 p-2 rounded text-sm {% if 'prereq_needed' in msg %}bg-red-900/30 text-cyber-red{% else %}bg-green-900/30 text-cyber-green{% endif %}">
|
|
{% if msg == 'excluded' %}Serveur exclu.{% elif msg == 'restored' %}Serveur restaure.{% elif msg == 'prereq_saved' %}Prereqs sauvegardes.{% elif msg == 'prereq_checked' %}Prereq re-verifie.{% elif msg == 'prereq_needed' %}Impossible de planifier : tous les serveurs pending doivent avoir leurs prereqs valides.{% elif msg.startswith('checked_') %}Verification terminee : {{ msg.split('_')[1] }} serveur(s) verifies, {{ msg.split('_')[2] }} auto-exclus.{% elif msg.startswith('auto_excluded_') %}{{ msg.split('_')[-1] }} serveur(s) exclus (prereqs KO).{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- KPIs -->
|
|
<div class="grid grid-cols-7 gap-3 mb-4">
|
|
<div class="card p-3 text-center">
|
|
<div class="text-2xl font-bold text-cyber-accent">{{ stats.total }}</div>
|
|
<div class="text-xs text-gray-500">Total</div>
|
|
</div>
|
|
<div class="card p-3 text-center">
|
|
<div class="text-2xl font-bold text-cyber-green">{{ stats.patched }}</div>
|
|
<div class="text-xs text-gray-500">Patches</div>
|
|
</div>
|
|
<div class="card p-3 text-center">
|
|
<div class="text-2xl font-bold text-cyber-red">{{ stats.failed }}</div>
|
|
<div class="text-xs text-gray-500">Echoues</div>
|
|
</div>
|
|
<div class="card p-3 text-center">
|
|
<div class="text-2xl font-bold text-cyber-yellow">{{ stats.pending }}</div>
|
|
<div class="text-xs text-gray-500">En attente</div>
|
|
</div>
|
|
<div class="card p-3 text-center">
|
|
<div class="text-2xl font-bold text-gray-500">{{ stats.excluded }}</div>
|
|
<div class="text-xs text-gray-500">Exclus</div>
|
|
</div>
|
|
<div class="card p-3 text-center">
|
|
<div class="text-2xl font-bold text-blue-400">{{ stats.reported }}</div>
|
|
<div class="text-xs text-gray-500">Reportes</div>
|
|
</div>
|
|
<div class="card p-3 text-center">
|
|
{% set patchable = stats.total - stats.excluded - stats.cancelled %}
|
|
{% if patchable > 0 %}
|
|
<div class="text-2xl font-bold text-cyber-accent">{{ (stats.patched / patchable * 100)|int }}%</div>
|
|
{% else %}<div class="text-2xl font-bold text-gray-600">-</div>{% endif %}
|
|
<div class="text-xs text-gray-500">Progression</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Prereqs stats (draft only) -->
|
|
{% if c.status == 'draft' and prereq %}
|
|
<div class="card p-4 mb-4">
|
|
<div class="flex justify-between items-center mb-2">
|
|
<h3 class="text-sm font-bold text-cyber-accent">Prerequis ({{ prereq.prereq_ok }}/{{ prereq.total_pending }} valides)</h3>
|
|
<div class="flex gap-2">
|
|
<form method="POST" action="/campaigns/{{ c.id }}/check-prereqs">
|
|
<button class="btn-primary px-3 py-1 text-sm" onclick="this.textContent='Verification en cours...'; this.disabled=true; this.form.submit()">Verifier les prereqs</button>
|
|
</form>
|
|
{% if prereq.prereq_ko > 0 %}
|
|
<form method="POST" action="/campaigns/{{ c.id }}/auto-exclude-failed">
|
|
<button class="btn-sm bg-red-900/30 text-cyber-red" onclick="return confirm('Exclure les {{ prereq.prereq_ko }} serveurs en echec ?')">Exclure {{ prereq.prereq_ko }} KO</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-5 gap-3 text-sm">
|
|
<div class="flex justify-between"><span class="text-gray-500">A verifier</span><span class="text-cyber-yellow">{{ prereq.prereq_todo }}</span></div>
|
|
<div class="flex justify-between"><span class="text-gray-500">SSH OK</span><span class="text-cyber-green">{{ prereq.ssh_ok }}</span></div>
|
|
<div class="flex justify-between"><span class="text-gray-500">Satellite OK</span><span class="text-cyber-green">{{ prereq.sat_ok }}</span></div>
|
|
<div class="flex justify-between"><span class="text-gray-500">Rollback OK</span><span class="text-cyber-green">{{ prereq.rollback_ok }}</span></div>
|
|
<div class="flex justify-between"><span class="text-gray-500">Disque OK</span><span class="text-cyber-green">{{ prereq.disk_ok }}</span></div>
|
|
</div>
|
|
{% if prereq.total_pending > 0 %}
|
|
<div class="w-full h-2 bg-gray-800 rounded-full overflow-hidden mt-2">
|
|
<div class="h-full bg-cyber-green" style="width: {{ (prereq.prereq_ok / prereq.total_pending * 100)|int }}%"></div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Table serveurs -->
|
|
<div x-data="{ excluding: null, prereqing: null }" class="card overflow-x-auto">
|
|
<table class="w-full table-cyber">
|
|
<thead><tr>
|
|
<th class="text-left p-2">Hostname</th>
|
|
<th class="p-2">Domaine</th>
|
|
<th class="p-2">Env</th>
|
|
<th class="p-2">OS</th>
|
|
<th class="p-2">Licence</th>
|
|
{% if c.status == 'draft' %}
|
|
<th class="p-2">SSH</th>
|
|
<th class="p-2">Satellite</th>
|
|
<th class="p-2">Rollback</th>
|
|
<th class="p-2">Disque</th>
|
|
<th class="p-2">Prereq</th>
|
|
{% endif %}
|
|
<th class="p-2">Statut</th>
|
|
<th class="p-2">Actions</th>
|
|
</tr></thead>
|
|
<tbody>
|
|
{% for s in sessions %}
|
|
<tr id="row-{{ s.id }}" class="{% if s.status == 'excluded' %}opacity-40{% elif s.status == 'patched' %}opacity-60{% elif s.status == 'failed' %}bg-red-900/10{% 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{% else %}badge-yellow{% endif %}">{{ (s.environnement or '-')[:6] }}</span></td>
|
|
<td class="p-2 text-center text-xs">{{ s.os_family or '-' }}</td>
|
|
<td class="p-2 text-center"><span class="badge {% if s.licence_support == 'active' %}badge-green{% elif s.licence_support == 'eol' %}badge-red{% else %}badge-yellow{% endif %}">{{ s.licence_support }}</span></td>
|
|
{% if c.status == 'draft' %}
|
|
<td class="p-2 text-center">
|
|
{% if s.prereq_ssh == 'ok' %}<span class="text-cyber-green text-xs">OK</span>
|
|
{% elif s.prereq_ssh == 'ko' %}<span class="text-cyber-red text-xs">KO</span>
|
|
{% else %}<span class="text-gray-600 text-xs">-</span>{% endif %}
|
|
</td>
|
|
<td class="p-2 text-center">
|
|
{% if s.prereq_satellite == 'ok' %}<span class="text-cyber-green text-xs">OK</span>
|
|
{% elif s.prereq_satellite == 'ko' %}<span class="text-cyber-red text-xs">KO</span>
|
|
{% elif s.prereq_satellite == 'na' %}<span class="text-gray-500 text-xs">N/A</span>
|
|
{% else %}<span class="text-gray-600 text-xs">-</span>{% endif %}
|
|
</td>
|
|
<td class="p-2 text-center">
|
|
{% if s.rollback_method %}<span class="badge {% if s.rollback_method == 'force' %}badge-red{% else %}badge-green{% endif %}">{{ s.rollback_method }}</span>
|
|
{% else %}<span class="text-gray-600 text-xs">-</span>{% endif %}
|
|
</td>
|
|
<td class="p-2 text-center text-xs">
|
|
{% if s.prereq_disk_ok is true %}<span class="text-cyber-green" title="/ {{ s.prereq_disk_root_mb or '?' }}Mo | /var {{ s.prereq_disk_var_mb or '?' }}Mo">OK</span>
|
|
{% elif s.prereq_disk_ok is false %}<span class="text-cyber-red" title="/ {{ s.prereq_disk_root_mb or '?' }}Mo | /var {{ s.prereq_disk_var_mb or '?' }}Mo">KO</span>
|
|
{% else %}<span class="text-gray-600">-</span>{% endif %}
|
|
</td>
|
|
<td class="p-2 text-center">
|
|
{% if s.prereq_validated %}<span class="badge badge-green">OK</span>
|
|
{% elif s.prereq_date %}<span class="badge badge-red">KO</span>
|
|
{% else %}<span class="text-gray-600 text-xs">-</span>{% endif %}
|
|
</td>
|
|
{% endif %}
|
|
<td class="p-2 text-center">
|
|
<span class="badge {% if s.status == 'patched' %}badge-green{% elif s.status == 'failed' %}badge-red{% elif s.status == 'excluded' %}badge-gray{% elif s.status == 'in_progress' %}badge-yellow{% else %}badge-gray{% endif %}">{{ s.status }}</span>
|
|
{% if s.exclusion_reason %}
|
|
<div class="text-[10px] text-gray-500 mt-0.5" title="{{ s.exclusion_detail or '' }}">
|
|
{% if s.exclusion_reason == 'eol' %}EOL
|
|
{% elif s.exclusion_reason == 'creneau_inadequat' %}Creneau/Prereq
|
|
{% elif s.exclusion_reason == 'intervention_non_secops' %}Non-SecOps
|
|
{% elif s.exclusion_reason == 'report_cycle' %}Reporte
|
|
{% elif s.exclusion_reason == 'non_patchable' %}Non patchable
|
|
{% else %}{{ s.exclusion_reason }}{% endif %}
|
|
{% if s.excluded_by %}<span class="text-gray-600">({{ s.excluded_by }})</span>{% endif %}
|
|
</div>
|
|
{% if s.exclusion_detail %}<div class="text-[9px] text-gray-600 italic">{{ s.exclusion_detail[:60] }}</div>{% endif %}
|
|
{% endif %}
|
|
</td>
|
|
<td class="p-2 text-center text-xs">
|
|
{% if s.status == 'excluded' %}
|
|
<form method="POST" action="/campaigns/session/{{ s.id }}/restore" style="display:inline">
|
|
<button class="btn-sm bg-green-900/30 text-cyber-green">Restaurer</button>
|
|
</form>
|
|
{% elif s.status == 'pending' and c.status == 'draft' %}
|
|
<div class="flex gap-1 justify-center">
|
|
<form method="POST" action="/campaigns/session/{{ s.id }}/check-prereq" style="display:inline">
|
|
<button class="btn-sm bg-cyber-border text-cyber-accent" title="Re-verifier ce serveur">Check</button>
|
|
</form>
|
|
<button @click="prereqing = prereqing === {{ s.id }} ? null : {{ s.id }}" class="btn-sm bg-cyber-border text-cyber-accent">Edit</button>
|
|
<button @click="excluding = excluding === {{ s.id }} ? null : {{ s.id }}" class="btn-sm bg-cyber-border text-gray-400">Exclure</button>
|
|
</div>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
<!-- Formulaire prereq inline -->
|
|
{% if s.status == 'pending' and c.status == 'draft' %}
|
|
<tr x-show="prereqing === {{ s.id }}">
|
|
<td colspan="12" class="p-2 bg-cyber-bg">
|
|
<form method="POST" action="/campaigns/session/{{ s.id }}/prereq" class="flex gap-2 items-center flex-wrap">
|
|
<span class="text-xs text-gray-500">SSH:</span>
|
|
<select name="prereq_ssh" class="text-xs py-1 px-2">
|
|
<option value="ok" {% if s.prereq_ssh == 'ok' %}selected{% endif %}>OK</option>
|
|
<option value="ko" {% if s.prereq_ssh == 'ko' %}selected{% endif %}>KO</option>
|
|
<option value="pending" {% if s.prereq_ssh == 'pending' %}selected{% endif %}>Pending</option>
|
|
</select>
|
|
<span class="text-xs text-gray-500">Satellite:</span>
|
|
<select name="prereq_satellite" class="text-xs py-1 px-2">
|
|
<option value="ok" {% if s.prereq_satellite == 'ok' %}selected{% endif %}>OK</option>
|
|
<option value="ko" {% if s.prereq_satellite == 'ko' %}selected{% endif %}>KO</option>
|
|
<option value="na" {% if s.prereq_satellite == 'na' %}selected{% endif %}>N/A</option>
|
|
<option value="pending" {% if s.prereq_satellite == 'pending' %}selected{% endif %}>Pending</option>
|
|
</select>
|
|
<span class="text-xs text-gray-500">Rollback:</span>
|
|
<select name="rollback_method" class="text-xs py-1 px-2">
|
|
<option value="">-</option>
|
|
<option value="snapshot" {% if s.rollback_method == 'snapshot' %}selected{% endif %}>Snapshot vSphere</option>
|
|
<option value="commvault" {% if s.rollback_method == 'commvault' %}selected{% endif %}>Commvault</option>
|
|
<option value="commcell" {% if s.rollback_method == 'commcell' %}selected{% endif %}>CommCell</option>
|
|
<option value="force" {% if s.rollback_method == 'force' %}selected{% endif %}>Force (justif.)</option>
|
|
<option value="na" {% if s.rollback_method == 'na' %}selected{% endif %}>N/A (physique)</option>
|
|
</select>
|
|
<input type="text" name="rollback_justif" value="{{ s.rollback_justif or '' }}" placeholder="Justification si force" class="text-xs py-1 px-2 flex-1">
|
|
<button type="submit" class="btn-sm bg-cyber-accent text-black">Valider</button>
|
|
<button type="button" @click="prereqing = null" class="btn-sm bg-cyber-border text-gray-400">X</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
<!-- Formulaire exclusion inline -->
|
|
<tr x-show="excluding === {{ s.id }}">
|
|
<td colspan="12" class="p-2 bg-cyber-bg">
|
|
<form method="POST" action="/campaigns/session/{{ s.id }}/exclude" class="flex gap-2 items-center flex-wrap">
|
|
<span class="text-xs text-gray-500">Motif:</span>
|
|
<select name="reason" required class="text-xs py-1 px-2">
|
|
{% for code, label in exclusion_reasons %}
|
|
<option value="{{ code }}">{{ label }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
<input type="text" name="detail" placeholder="Detail / justification" class="text-xs py-1 px-2 flex-1">
|
|
<button type="submit" class="btn-sm bg-red-900/30 text-cyber-red">Confirmer</button>
|
|
<button type="button" @click="excluding = null" class="btn-sm bg-cyber-border text-gray-400">X</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% endblock %}
|