- Permissions 100% depuis user_permissions (plus de hardcode) - Middleware injecte perms dans chaque requête - Créneaux auto: 09h-12h30 / 14h-16h45, pas 15min, hprod lun-mar, prod mer-jeu - Assignations par défaut: par domaine, app_type, zone, serveur (table default_assignments) - Auto-liaison app_group: même intervenant recette+prod - Audit Splunk: /var/log/patchcenter_audit.json (JSON one-line par event) - Login/logout/campagnes/prereqs loggés en base + fichier - Page erreur maintenance (500/404) avec contact SecOps - Accents français dans toute lUI - Operator affiché comme Intervenant - Session 1h, redirect / vers dashboard si connecté - Demo mode prereqs (DEMO_MODE=True) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
92 lines
5.0 KiB
HTML
92 lines
5.0 KiB
HTML
{% extends 'base.html' %}
|
|
{% block title %}Campagnes{% endblock %}
|
|
{% block content %}
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h2 class="text-xl font-bold text-cyber-accent">Campagnes <span class="text-sm text-gray-500">{{ year }}</span></h2>
|
|
<div class="flex gap-2 items-center">
|
|
<a href="?year={{ year - 1 }}" class="btn-sm bg-cyber-border text-gray-300">{{ year - 1 }}</a>
|
|
<a href="?year={{ year + 1 }}" class="btn-sm bg-cyber-border text-gray-300">{{ year + 1 }}</a>
|
|
</div>
|
|
</div>
|
|
|
|
{% set msg = request.query_params.get('msg') %}
|
|
{% if msg %}
|
|
<div class="mb-4 p-2 rounded text-sm {% if msg in ('already_exists','no_servers','create_error') %}bg-red-900/30 text-cyber-red{% else %}bg-green-900/30 text-cyber-green{% endif %}">
|
|
{% if msg == 'deleted' %}Campagne supprimée.{% elif msg == 'already_exists' %}Une campagne existe déjà pour cette semaine. Supprimez-la d'abord.{% elif msg == 'no_servers' %}Aucun serveur éligible pour cette semaine.{% elif msg == 'create_error' %}Erreur à la création. Vérifiez les logs.{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Filtres statut -->
|
|
<div class="flex gap-2 mb-4">
|
|
<a href="?year={{ year }}" class="btn-sm {% if not status_filter %}bg-cyber-accent text-black{% else %}bg-cyber-border text-gray-300{% endif %}">Toutes</a>
|
|
{% for st in ['draft','planned','in_progress','completed','cancelled'] %}
|
|
<a href="?year={{ year }}&status={{ st }}" class="btn-sm {% if status_filter == st %}bg-cyber-accent text-black{% else %}bg-cyber-border text-gray-300{% endif %}">{{ st }}</a>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<!-- Liste campagnes -->
|
|
<div class="space-y-2">
|
|
{% for c in campaigns %}
|
|
<a href="/campaigns/{{ c.id }}" class="card p-4 flex items-center justify-between hover:border-cyber-accent/50 transition-colors block">
|
|
<div class="flex items-center gap-4">
|
|
<span class="font-bold text-cyber-accent">{{ c.week_code }}</span>
|
|
<span class="text-sm text-gray-400">{{ c.label or '' }}</span>
|
|
<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>
|
|
{% if c.date_start %}
|
|
<span class="text-xs text-gray-500">{{ c.date_start.strftime('%d/%m') }}{% if c.date_end %} → {{ c.date_end.strftime('%d/%m') }}{% endif %}</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<div class="flex gap-1 text-xs">
|
|
<span class="px-2 py-0.5 rounded bg-gray-800 text-gray-400">{{ c.session_count }} srv</span>
|
|
{% if c.patched_count %}<span class="px-2 py-0.5 rounded bg-green-900/30 text-cyber-green">{{ c.patched_count }} ok</span>{% endif %}
|
|
{% if c.failed_count %}<span class="px-2 py-0.5 rounded bg-red-900/30 text-cyber-red">{{ c.failed_count }} ko</span>{% endif %}
|
|
{% if c.excluded_count %}<span class="px-2 py-0.5 rounded bg-yellow-900/30 text-gray-400">{{ c.excluded_count }} excl</span>{% endif %}
|
|
</div>
|
|
{% if c.session_count > 0 %}
|
|
<div class="w-20 h-2 bg-gray-800 rounded-full overflow-hidden">
|
|
<div class="h-full bg-cyber-green" style="width: {{ (c.patched_count / c.session_count * 100)|int }}%"></div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</a>
|
|
{% endfor %}
|
|
{% if not campaigns %}
|
|
<div class="card p-8 text-center text-gray-500">Aucune campagne pour {{ year }}</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Nouvelle campagne depuis le planning (admin/coordinateur seulement) -->
|
|
{% if perms.campaigns in ('edit', 'admin') %}
|
|
<div x-data="{ showCreate: false }" class="mt-6">
|
|
<button @click="showCreate = !showCreate" class="btn-primary px-4 py-2 text-sm">Nouvelle campagne</button>
|
|
|
|
<div x-show="showCreate" class="card p-5 mt-3 space-y-4">
|
|
<h3 class="text-lg font-bold text-cyber-accent">Créer depuis le planning</h3>
|
|
|
|
{% if planned_weeks %}
|
|
<div>
|
|
<label class="text-xs text-gray-500">Semaine planifiee</label>
|
|
<div id="campaign-scope">
|
|
<input type="hidden" name="year" value="{{ year }}">
|
|
<select name="week" class="w-full"
|
|
hx-get="/campaigns/preview" hx-target="#preview-zone" hx-swap="innerHTML"
|
|
hx-include="#campaign-scope">
|
|
<option value="">Choisir une semaine...</option>
|
|
{% for w in planned_weeks %}
|
|
<option value="{{ w.week_number }}">
|
|
{{ w.week_code }} ({{ w.week_start.strftime('%d/%m') }} → {{ w.week_end.strftime('%d/%m') }}) — {{ w.scope }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div id="preview-zone"></div>
|
|
{% else %}
|
|
<p class="text-gray-500 text-sm">Aucune semaine planifiee a venir pour {{ year }}. Vérifiez le planning.</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
{% endblock %}
|