323 lines
22 KiB
HTML
323 lines
22 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 == 'pending_validation' %}badge-yellow{% 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 can_edit_campaigns %}
|
|
{% if c.status == 'draft' %}
|
|
{% if can_plan %}
|
|
<form method="POST" action="/campaigns/{{ c.id }}/status"><input type="hidden" name="new_status" value="pending_validation">
|
|
<button class="btn-primary px-4 py-2 text-sm">Soumettre au COMEP</button></form>
|
|
{% else %}
|
|
<button class="btn-sm bg-gray-700 text-gray-500 px-4 py-2 cursor-not-allowed">COMEP (prereqs requis)</button>
|
|
{% endif %}
|
|
{% elif c.status == 'pending_validation' %}
|
|
<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 (post-COMEP)</button></form>
|
|
<form method="POST" action="/campaigns/{{ c.id }}/status"><input type="hidden" name="new_status" value="draft">
|
|
<button class="btn-sm bg-cyber-border text-gray-300 px-4 py-2">Retour draft</button></form>
|
|
{% 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', 'pending_validation', '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 cette campagne ?')">Annuler</button></form>
|
|
{% endif %}
|
|
{% if c.status in ('draft', 'cancelled') %}
|
|
<form method="POST" action="/campaigns/{{ c.id }}/delete">
|
|
<button class="btn-sm bg-red-900/50 text-cyber-red px-4 py-2" onclick="return confirm('SUPPRIMER definitivement cette campagne ? Cette action est irreversible.')">Supprimer</button></form>
|
|
{% endif %}
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
{% if msg %}
|
|
<div class="mb-3 p-2 rounded text-sm {% if msg in ('prereq_needed','already_taken','limit_reached') %}bg-red-900/30 text-cyber-red{% else %}bg-green-900/30 text-cyber-green{% endif %}">
|
|
{% if msg == 'bulk_taken' %}Serveurs pris.{% elif msg == 'bulk_assigned' %}Serveurs assignés.{% elif msg == 'bulk_excluded' %}Serveurs exclus.{% elif msg == 'excluded' %}Serveur exclu.{% elif msg == 'restored' %}Serveur restaure.{% elif msg == 'prereq_saved' %}Prereqs sauvegardes.{% elif msg == 'prereq_checked' %}Prereq verifie.{% elif msg == 'prereq_needed' %}Prereqs requis avant soumission COMEP.{% elif msg == 'taken' %}Serveur pris.{% elif msg == 'released' %}Serveur libere.{% elif msg == 'assigned' %}Operateur assigne.{% elif msg == 'scheduled' %}Planning ajuste.{% elif msg == 'limit_set' %}Limite operateur definie.{% elif msg == 'already_taken' %}Ce serveur est deja pris.{% elif msg == 'limit_reached' %}Limite de serveurs atteinte pour cette campagne.{% elif msg == 'forced_cant_release' %}Assignation forcee — seul le coordinateur peut modifier.{% elif msg.startswith('checked_') %}Verification: {{ msg.split('_')[1] }} verifies, {{ msg.split('_')[2] }} auto-exclus.{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- KPIs -->
|
|
<div class="flex gap-2 mb-4 flex-wrap">
|
|
<div class="card p-2 text-center"><div class="text-xl font-bold text-cyber-accent">{{ stats.total }}</div><div class="text-[10px] text-gray-500">Total</div></div>
|
|
<div class="card p-2 text-center"><div class="text-xl font-bold text-cyber-green">{{ stats.patched }}</div><div class="text-[10px] text-gray-500">Patches</div></div>
|
|
<div class="card p-2 text-center"><div class="text-xl font-bold text-cyber-red">{{ stats.failed }}</div><div class="text-[10px] text-gray-500">Echoues</div></div>
|
|
<div class="card p-2 text-center"><div class="text-xl font-bold text-cyber-yellow">{{ stats.pending }}</div><div class="text-[10px] text-gray-500">En attente</div></div>
|
|
<div class="card p-2 text-center"><div class="text-xl font-bold text-gray-500">{{ stats.excluded }}</div><div class="text-[10px] text-gray-500">Exclus</div></div>
|
|
<div class="card p-2 text-center"><div class="text-xl font-bold text-cyber-accent">{{ stats.assigned }}</div><div class="text-[10px] text-gray-500">Assignes</div></div>
|
|
<div class="card p-2 text-center"><div class="text-xl font-bold text-gray-400">{{ stats.unassigned }}</div><div class="text-[10px] text-gray-500">Libres</div></div>
|
|
<div class="card p-2 text-center"><div class="text-xl font-bold text-cyber-red">{{ stats.dmz }}</div><div class="text-[10px] text-gray-500">DMZ</div></div>
|
|
<div class="card p-2 text-center">
|
|
{% set patchable = stats.total - stats.excluded - stats.cancelled %}
|
|
<div class="text-xl font-bold text-cyber-accent">{% if patchable > 0 %}{{ (stats.patched / patchable * 100)|int }}%{% else %}-{% endif %}</div>
|
|
<div class="text-[10px] text-gray-500">Progression</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Repartition operateurs -->
|
|
{% if op_counts %}
|
|
<div class="flex gap-2 mb-4 flex-wrap">
|
|
{% for oc in op_counts %}
|
|
<div class="card px-3 py-1 flex items-center gap-2">
|
|
<span class="text-sm {% if oc.display_name == user.sub %}text-cyber-accent font-bold{% else %}text-gray-300{% endif %}">{{ oc.display_name }}</span>
|
|
<span class="badge badge-blue">{{ oc.count }}</span>
|
|
</div>
|
|
{% endfor %}
|
|
{% if stats.unassigned > 0 %}
|
|
<div class="card px-3 py-1 flex items-center gap-2">
|
|
<span class="text-sm text-gray-500">Non assignes</span>
|
|
<span class="badge badge-gray">{{ stats.unassigned }}</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Prereqs (draft) -->
|
|
{% if c.status == 'draft' and prereq and can_edit_campaigns %}
|
|
<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>
|
|
<form method="POST" action="/campaigns/{{ c.id }}/check-prereqs">
|
|
<button class="btn-primary px-3 py-1 text-sm">Verifier prereqs</button>
|
|
</form>
|
|
</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</span><span class="text-cyber-green">{{ prereq.ssh_ok }}</span></div>
|
|
<div class="flex justify-between"><span class="text-gray-500">Satellite</span><span class="text-cyber-green">{{ prereq.sat_ok }}</span></div>
|
|
<div class="flex justify-between"><span class="text-gray-500">Rollback</span><span class="text-cyber-green">{{ prereq.rollback_ok }}</span></div>
|
|
<div class="flex justify-between"><span class="text-gray-500">Disque</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 %}
|
|
|
|
<!-- Actions groupées -->
|
|
<div id="bulk-campaign-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-camp-count">0 sélectionné(s)</span>
|
|
|
|
{% if c.status in ('planned', 'in_progress') %}
|
|
{# Prendre en masse (opérateur) #}
|
|
<form method="POST" action="/campaigns/{{ c.id }}/bulk/take" style="display:inline">
|
|
<input type="hidden" name="session_ids" id="bulk-camp-ids-take">
|
|
<button class="btn-sm bg-cyber-accent text-black">Prendre la sélection</button>
|
|
</form>
|
|
{% endif %}
|
|
|
|
{% if can_edit_campaigns %}
|
|
{# Assigner en masse (coordinateur) #}
|
|
<form method="POST" action="/campaigns/{{ c.id }}/bulk/assign" class="flex gap-1 items-center">
|
|
<input type="hidden" name="session_ids" id="bulk-camp-ids-assign">
|
|
<select name="operator_id" class="text-xs py-1 px-2">
|
|
<option value="">— Intervenant —</option>
|
|
{% for u in intervenants %}<option value="{{ u.id }}">{{ u.display_name }}</option>{% endfor %}
|
|
</select>
|
|
<button class="btn-sm bg-cyber-border text-cyber-accent">Assigner</button>
|
|
</form>
|
|
|
|
{# Exclure en masse #}
|
|
<form method="POST" action="/campaigns/{{ c.id }}/bulk/exclude" class="flex gap-1 items-center">
|
|
<input type="hidden" name="session_ids" id="bulk-camp-ids-excl">
|
|
<select name="reason" class="text-xs py-1 px-2">
|
|
{% for code, label in exclusion_reasons %}<option value="{{ code }}">{{ label }}</option>{% endfor %}
|
|
</select>
|
|
<button class="btn-sm bg-red-900/30 text-cyber-red">Exclure</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<script>
|
|
function updateBulkCamp() {
|
|
var checks = document.querySelectorAll('input[name=sess_chk]:checked');
|
|
var bar = document.getElementById('bulk-campaign-bar');
|
|
if (checks.length > 0) {
|
|
bar.style.display = 'flex';
|
|
document.getElementById('bulk-camp-count').textContent = checks.length + ' sélectionné(s)';
|
|
var ids = Array.from(checks).map(function(c) { return c.value; }).join(',');
|
|
['take','assign','excl'].forEach(function(t) {
|
|
var el = document.getElementById('bulk-camp-ids-' + t);
|
|
if (el) el.value = ids;
|
|
});
|
|
} else { bar.style.display = 'none'; }
|
|
}
|
|
function showForm(id, type) {
|
|
document.querySelectorAll('.inline-form').forEach(function(el) { el.style.display = 'none'; });
|
|
var el = document.getElementById('form-' + type + '-' + id);
|
|
if (el) el.style.display = '';
|
|
}
|
|
</script>
|
|
|
|
<!-- Table serveurs -->
|
|
<table class="w-full table-cyber text-xs">
|
|
<thead><tr>
|
|
<th class="p-2 w-6"><input type="checkbox" onchange="document.querySelectorAll('input[name=sess_chk]').forEach(function(c){c.checked=this.checked}.bind(this)); updateBulkCamp()"></th>
|
|
<th class="text-left p-2">Hostname</th>
|
|
<th class="p-2">Domaine</th>
|
|
<th class="p-2">Env</th>
|
|
<th class="p-2">Zone</th>
|
|
<th class="p-2">Tier</th>
|
|
<th class="p-2">Jour prevu</th>
|
|
<th class="p-2">Heure</th>
|
|
<th class="p-2">Operateur</th>
|
|
{% if c.status == 'draft' %}
|
|
<th class="p-2">SSH</th>
|
|
<th class="p-2">Sat</th>
|
|
<th class="p-2">Disque</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 text-center" onclick="event.stopPropagation()">{% if s.status == 'pending' %}<input type="checkbox" name="sess_chk" value="{{ s.id }}" onchange="updateBulkCamp()">{% endif %}</td>
|
|
<td class="p-2 font-mono text-cyber-accent">{{ s.hostname }}</td>
|
|
<td class="p-2 text-center">{{ s.domaine or '-' }}</td>
|
|
<td class="p-2 text-center"><span class="badge {% if s.environnement == 'Production' %}badge-green{% else %}badge-yellow{% endif %}"title="{{ s.environnement or '' }}">{{ (s.environnement or '-')[:6] }}</span></td>
|
|
<td class="p-2 text-center"><span class="badge {% if s.zone_name == 'DMZ' %}badge-red{% elif s.zone_name == 'EMV' %}badge-yellow{% else %}badge-blue{% endif %}">{{ s.zone_name or 'LAN' }}</span></td>
|
|
<td class="p-2 text-center"><span class="badge {% if s.tier == 'tier0' %}badge-red{% elif s.tier == 'tier1' %}badge-yellow{% else %}badge-blue{% endif %}">{{ s.tier }}</span></td>
|
|
<td class="p-2 text-center">{% if s.date_prevue %}{% set jours = {0:'Lun',1:'Mar',2:'Mer',3:'Jeu',4:'Ven',5:'Sam',6:'Dim'} %}{{ jours[s.date_prevue.weekday()] }} {{ s.date_prevue.strftime('%d/%m') }}{% else %}-{% endif %}</td>
|
|
<td class="p-2 text-center text-gray-400">{{ s.heure_prevue or s.pref_patch_heure or '-' }}</td>
|
|
<td class="p-2 text-center">
|
|
{% if s.intervenant_name %}
|
|
<span class="text-cyber-accent">{{ s.intervenant_name }}</span>
|
|
{% if s.forced_assignment %}<span class="text-cyber-yellow text-[9px] ml-0.5" title="Assignation forcee">🔒</span>{% endif %}
|
|
{% else %}<span class="text-gray-600">—</span>{% endif %}
|
|
</td>
|
|
{% if c.status == 'draft' %}
|
|
<td class="p-2 text-center">{% if s.prereq_ssh == 'ok' %}<span class="text-cyber-green">OK</span>{% elif s.prereq_ssh == 'ko' %}<span class="text-cyber-red">KO</span>{% else %}-{% endif %}</td>
|
|
<td class="p-2 text-center">{% if s.prereq_satellite == 'ok' %}<span class="text-cyber-green">OK</span>{% elif s.prereq_satellite == 'ko' %}<span class="text-cyber-red">KO</span>{% else %}-{% endif %}</td>
|
|
<td class="p-2 text-center">{% if s.prereq_disk_ok is true %}<span class="text-cyber-green">OK</span>{% elif s.prereq_disk_ok is false %}<span class="text-cyber-red">KO</span>{% else %}-{% 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-[9px] text-gray-500" title="{{ s.exclusion_detail or '' }}">
|
|
{% if s.exclusion_reason == 'obsolete' %}EOL{% elif s.exclusion_reason == 'creneau_inadequat' %}Prereq KO{% 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>
|
|
{% endif %}
|
|
</td>
|
|
<td class="p-2 text-center">
|
|
{% if s.status == 'excluded' and can_edit_campaigns %}
|
|
<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' %}
|
|
{% if c.status in ('planned', 'in_progress') %}
|
|
{# Operateur: prendre/liberer/planifier #}
|
|
{% if not s.intervenant_id %}
|
|
<form method="POST" action="/campaigns/session/{{ s.id }}/take" style="display:inline"><button class="btn-sm bg-cyber-accent text-black">Prendre</button></form>
|
|
{% elif s.intervenant_id == user.uid %}
|
|
{% if not s.forced_assignment %}
|
|
<form method="POST" action="/campaigns/session/{{ s.id }}/release" style="display:inline"><button class="btn-sm bg-cyber-border text-gray-400">Libérer</button></form>
|
|
{% endif %}
|
|
<button onclick="showForm({{ s.id }}, 'schedule')" class="btn-sm bg-cyber-border text-gray-400">Planifier</button>
|
|
{% endif %}
|
|
{# Coordinateur: assigner + planifier + exclure #}
|
|
{% if can_edit_campaigns %}
|
|
<button onclick="showForm({{ s.id }}, 'assign')" class="btn-sm bg-cyber-border text-cyber-accent">Assigner</button>
|
|
{% if s.intervenant_id != user.uid %}<button onclick="showForm({{ s.id }}, 'schedule')" class="btn-sm bg-cyber-border text-gray-400">Planifier</button>{% endif %}
|
|
<button onclick="showForm({{ s.id }}, 'exclude')" class="btn-sm bg-cyber-border text-gray-400">Exclure</button>
|
|
{% endif %}
|
|
|
|
{% elif c.status in ('draft', 'pending_validation') and can_edit_campaigns %}
|
|
<div class="flex gap-1 justify-center">
|
|
{% if c.status == 'draft' %}
|
|
<form method="POST" action="/campaigns/session/{{ s.id }}/check-prereq" style="display:inline"><button class="btn-sm bg-cyber-border text-cyber-accent">Check</button></form>
|
|
{% endif %}
|
|
<button onclick="showForm({{ s.id }}, 'exclude')" class="btn-sm bg-cyber-border text-gray-400">Exclure</button>
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
|
|
{# Formulaires inline #}
|
|
{% if s.status == 'pending' and c.status in ('draft', 'pending_validation', 'planned', 'in_progress') %}
|
|
<tr id="form-exclude-{{ s.id }}" class="bg-cyber-bg inline-form" style="display:none">
|
|
<td colspan="12" class="p-2">
|
|
<form method="POST" action="/campaigns/session/{{ s.id }}/exclude" class="flex gap-2 items-center flex-wrap">
|
|
<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="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" onclick="this.closest('.inline-form').style.display='none'" class="btn-sm bg-cyber-border text-gray-400">X</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
<tr id="form-assign-{{ s.id }}" class="bg-cyber-bg inline-form" style="display:none">
|
|
<td colspan="12" class="p-2">
|
|
<form method="POST" action="/campaigns/session/{{ s.id }}/assign" class="flex gap-2 items-center">
|
|
<select name="operator_id" class="text-xs py-1 px-2">
|
|
<option value="">— Desassigner —</option>
|
|
{% for u in intervenants %}<option value="{{ u.id }}" {% if s.intervenant_id == u.id %}selected{% endif %}>{{ u.display_name }}</option>{% endfor %}
|
|
</select>
|
|
<label class="flex items-center gap-1 text-xs text-gray-400"><input type="checkbox" name="forced" {% if s.forced_assignment %}checked{% endif %}> Forcer</label>
|
|
<button type="submit" class="btn-sm bg-cyber-accent text-black">OK</button>
|
|
<button type="button" onclick="this.closest('.inline-form').style.display='none'" class="btn-sm bg-cyber-border text-gray-400">X</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
<tr id="form-schedule-{{ s.id }}" class="bg-cyber-bg inline-form" style="display:none">
|
|
<td colspan="12" class="p-2">
|
|
<form method="POST" action="/campaigns/session/{{ s.id }}/schedule" class="flex gap-2 items-center">
|
|
<input type="date" name="date_prevue" value="{{ s.date_prevue.strftime('%Y-%m-%d') if s.date_prevue else '' }}" class="text-xs py-1 px-2">
|
|
<input type="text" name="heure_prevue" value="{{ s.heure_prevue or '' }}" placeholder="ex: 9h00, 14h00" class="text-xs py-1 px-2 w-24">
|
|
<button type="submit" class="btn-sm bg-cyber-accent text-black">OK</button>
|
|
<button type="button" onclick="this.closest('.inline-form').style.display='none'" class="btn-sm bg-cyber-border text-gray-400">X</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Limites intervenants (coordinateur, planned) -->
|
|
{% if can_edit_campaigns and c.status in ('planned', 'pending_validation', 'in_progress') %}
|
|
<div class="card p-4 mt-4" style="max-width:400px">
|
|
<h3 class="text-sm font-bold text-cyber-accent mb-3">Limites intervenants</h3>
|
|
{% if op_limits %}
|
|
<div class="space-y-1 text-xs mb-3">
|
|
{% for ol in op_limits %}
|
|
<div class="flex justify-between items-center bg-cyber-bg p-2 rounded">
|
|
<span>{{ ol.display_name }}</span>
|
|
<span class="badge badge-yellow">max {{ ol.max_servers }}{% if ol.note %} — {{ ol.note }}{% endif %}</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
<form method="POST" action="/campaigns/{{ c.id }}/operator-limit" class="space-y-2">
|
|
<select name="operator_id" class="text-xs py-1 px-2 w-full">
|
|
{% for u in intervenants %}<option value="{{ u.id }}">{{ u.display_name }}</option>{% endfor %}
|
|
</select>
|
|
<div class="flex gap-2">
|
|
<input type="number" name="max_servers" min="0" value="5" class="text-xs py-1 px-2 w-20" placeholder="Max">
|
|
<input type="text" name="note" placeholder="Raison" class="text-xs py-1 px-2 flex-1">
|
|
</div>
|
|
<button type="submit" class="btn-primary px-3 py-1 text-sm w-full">Définir</button>
|
|
</form>
|
|
</div>
|
|
{% endif %}
|
|
{% endblock %}
|