- 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>
248 lines
14 KiB
HTML
248 lines
14 KiB
HTML
{% extends 'base.html' %}
|
|
{% block title %}Planning Patching {{ year }}{% endblock %}
|
|
{% block content %}
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h2 class="text-xl font-bold text-cyber-accent">Planning Patching {{ year }}</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>
|
|
<!-- Dupliquer (admin/coordinateur) -->
|
|
{% if entries and perms.planning in ('edit', 'admin') %}
|
|
<form method="POST" action="/planning/duplicate" class="flex gap-1 items-center ml-4">
|
|
<input type="hidden" name="source_year" value="{{ year }}">
|
|
<input type="number" name="target_year" value="{{ year + 1 }}" class="text-xs py-1 px-2 w-20">
|
|
<button type="submit" class="btn-sm bg-cyber-accent text-black" onclick="return confirm('Dupliquer {{ year }} vers cette annee ?')">Dupliquer vers</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
{% if msg %}
|
|
<div class="mb-4 p-2 rounded text-sm {% if msg in ('exists', 'err_week', 'err_domain', 'err_past', 'err_past_wed') %}bg-red-900/30 text-cyber-red{% else %}bg-green-900/30 text-cyber-green{% endif %}">
|
|
{% if msg == 'add' %}Entrée ajoutée.{% elif msg == 'edit' %}Entrée modifiée.{% elif msg == 'delete' %}Entrée supprimée.{% elif msg == 'duplicate' %}Planning dupliqué avec succès.{% elif msg == 'exists' %}L'annee cible contient déjà des entrées. Supprimez-les d'abord.{% elif msg == 'err_week' %}Numéro de semaine invalide (1-53).{% elif msg == 'err_domain' %}Domaine requis pour une entrée ouverte.{% elif msg == 'err_past' %}Impossible d'ajouter dans le passé (semaine déjà écoulée).{% elif msg == 'err_past_wed' %}Semaine en cours : ajout possible uniquement lundi et mardi (MEP urgente).{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Legende -->
|
|
<div class="flex gap-4 mb-4 text-xs items-center flex-wrap">
|
|
<div class="flex items-center gap-1"><span class="inline-block w-3 h-3 rounded" style="background:#1e3a8a"></span> Cycle 1</div>
|
|
<div class="flex items-center gap-1"><span class="inline-block w-3 h-3 rounded" style="background:#7c3aed"></span> Cycle 2</div>
|
|
<div class="flex items-center gap-1"><span class="inline-block w-3 h-3 rounded" style="background:#166534"></span> Cycle 3</div>
|
|
<div class="flex items-center gap-1"><span class="inline-block w-3 h-3 rounded" style="background:#f59e0b33"></span> Gel</div>
|
|
<div class="flex items-center gap-1"><span class="inline-block w-3 h-3 rounded" style="background:#D4A0A0"></span> DMZ (continu)</div>
|
|
<span class="text-gray-500 ml-2">HPROD = hors-prod | PROD = production | pilot = prod pilote</span>
|
|
</div>
|
|
|
|
<!-- Gantt -->
|
|
<div class="card overflow-x-auto">
|
|
<table class="table-cyber" style="min-width:1600px">
|
|
<!-- Mois header -->
|
|
<thead>
|
|
<tr>
|
|
<th class="p-2 text-left sticky left-0 bg-cyber-card z-10" style="min-width:140px">Domaine</th>
|
|
{% for m in months %}
|
|
<th colspan="{% if loop.index in [1,3,5,7,8,10,12] %}5{% elif loop.index == 2 %}4{% else %}4{% endif %}" class="p-1 text-center text-xs">{{ m }}</th>
|
|
{% endfor %}
|
|
</tr>
|
|
<!-- Semaines header -->
|
|
<tr>
|
|
<th class="p-1 sticky left-0 bg-cyber-card z-10"></th>
|
|
{% for w in weeks %}
|
|
<th class="p-0 text-center" style="width:22px;min-width:22px">
|
|
<span class="text-[9px] {% if w in freeze_weeks %}text-cyber-yellow{% else %}text-gray-600{% endif %}">{{ w }}</span>
|
|
</th>
|
|
{% endfor %}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for dom in domains %}
|
|
<tr>
|
|
<td class="p-2 sticky left-0 bg-cyber-card z-10 border-r border-cyber-border">
|
|
<div class="flex items-center gap-2">
|
|
<span class="inline-block w-2 h-6 rounded-sm" style="background:{{ domain_colors.get(dom.code, '#666') }}"></span>
|
|
<div>
|
|
<span class="font-bold text-sm" style="color:{{ domain_colors.get(dom.code, '#999') }}">{{ dom.name }}</span>
|
|
<span class="text-[10px] text-gray-500 ml-1">({{ dom.srv_count }})</span>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
{% for w in weeks %}
|
|
{% set entry = grid.get(dom.code, {}).get(w) %}
|
|
<td class="p-0 text-center" style="width:22px;min-width:22px">
|
|
{% if w in freeze_weeks %}
|
|
<div class="h-6" style="background:#f59e0b15" title="Gel S{{ w }}"></div>
|
|
{% elif entry %}
|
|
{% set bg = '#1e3a8a' %}
|
|
{% if entry.cycle == 2 %}{% set bg = '#7c3aed' %}{% endif %}
|
|
{% if entry.cycle == 3 %}{% set bg = '#166534' %}{% endif %}
|
|
{% if dom.code == 'DMZ' %}{% set bg = '#5f3737' %}{% endif %}
|
|
<div class="h-6 rounded-sm flex items-center justify-center cursor-pointer hover:opacity-80"
|
|
style="background:{{ bg }}"
|
|
title="S{{ w }} — {{ dom.name }} {{ entry.env_scope }}{% if entry.note %} — {{ entry.note }}{% endif %}">
|
|
<span class="text-[8px] text-white/80">
|
|
{% if entry.env_scope == 'prod' %}P{% elif entry.env_scope == 'hprod' %}H{% elif entry.env_scope == 'prod_pilot' %}PP{% elif entry.env_scope == 'all' %}A{% endif %}
|
|
</span>
|
|
</div>
|
|
{% else %}
|
|
<div class="h-6"></div>
|
|
{% endif %}
|
|
</td>
|
|
{% endfor %}
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Detail par cycle -->
|
|
<div class="grid grid-cols-3 gap-4 mt-6">
|
|
{% for cycle_num in [1, 2, 3] %}
|
|
<div class="card p-4">
|
|
<h3 class="text-sm font-bold text-cyber-accent mb-3">Cycle {{ cycle_num }}</h3>
|
|
<div class="space-y-1 text-xs">
|
|
{% for w in weeks %}
|
|
{% for dom in domains %}
|
|
{% set entry = grid.get(dom.code, {}).get(w) %}
|
|
{% if entry and entry.cycle == cycle_num and dom.code != 'DMZ' %}
|
|
<div class="flex justify-between items-center py-0.5">
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-gray-500 w-7">S{{ '%02d' % w }}</span>
|
|
<span class="inline-block w-2 h-2 rounded-full" style="background:{{ domain_colors.get(dom.code, '#666') }}"></span>
|
|
<span>{{ dom.name }}</span>
|
|
<span class="badge {% if entry.env_scope == 'prod' %}badge-green{% elif entry.env_scope == 'hprod' %}badge-yellow{% else %}badge-blue{% endif %}">{{ entry.env_scope }}</span>
|
|
</div>
|
|
{% if entry.note %}<span class="text-gray-600 truncate ml-1" style="max-width:100px" title="{{ entry.note }}">{{ entry.note[:20] }}</span>{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
{% endfor %}
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<!-- Tableau editable -->
|
|
<div x-data="{ editing: null }" class="card mt-6 overflow-x-auto">
|
|
<div class="flex justify-between items-center p-4 border-b border-cyber-border">
|
|
<h3 class="text-sm font-bold text-cyber-accent">Donnees planning {{ year }} ({{ entries|length }} entrees)</h3>
|
|
</div>
|
|
|
|
|
|
<table class="w-full table-cyber text-sm">
|
|
<thead><tr>
|
|
<th class="p-2">Sem.</th>
|
|
<th class="p-2">Dates</th>
|
|
<th class="p-2">Domaine</th>
|
|
<th class="p-2">Env</th>
|
|
<th class="p-2">Cycle</th>
|
|
<th class="p-2">Statut</th>
|
|
<th class="text-left p-2">Note</th>
|
|
{% if perms.planning in ('edit', 'admin') %}<th class="p-2">Actions</th>{% endif %}
|
|
</tr></thead>
|
|
<tbody>
|
|
{% for e in entries %}
|
|
<tr class="{% if e.status == 'freeze' %}bg-yellow-900/10{% endif %}">
|
|
<!-- Mode lecture -->
|
|
<template x-if="editing !== {{ e.id }}">
|
|
<td class="p-2 text-center font-mono text-xs">{{ e.week_code }}</td>
|
|
</template>
|
|
<template x-if="editing !== {{ e.id }}">
|
|
<td class="p-2 text-center text-xs text-gray-500">{{ e.week_start.strftime('%d/%m') }} - {{ e.week_end.strftime('%d/%m') }}</td>
|
|
</template>
|
|
<template x-if="editing !== {{ e.id }}">
|
|
<td class="p-2 text-center"><span style="color:{{ domain_colors.get(e.domain_code or '', '#666') }}">{{ e.domain_name or '-' }}</span></td>
|
|
</template>
|
|
<template x-if="editing !== {{ e.id }}">
|
|
<td class="p-2 text-center"><span class="badge {% if e.env_scope == 'prod' %}badge-green{% elif e.env_scope == 'hprod' %}badge-yellow{% else %}badge-blue{% endif %}">{{ e.env_scope }}</span></td>
|
|
</template>
|
|
<template x-if="editing !== {{ e.id }}">
|
|
<td class="p-2 text-center">{{ e.cycle or '-' }}</td>
|
|
</template>
|
|
<template x-if="editing !== {{ e.id }}">
|
|
<td class="p-2 text-center"><span class="badge {% if e.status == 'freeze' %}badge-yellow{% elif e.status == 'open' %}badge-green{% else %}badge-gray{% endif %}">{{ e.status }}</span></td>
|
|
</template>
|
|
<template x-if="editing !== {{ e.id }}">
|
|
<td class="p-2 text-xs text-gray-400">{{ e.note or '' }}</td>
|
|
</template>
|
|
{% if perms.planning in ('edit', 'admin') %}
|
|
<template x-if="editing !== {{ e.id }}">
|
|
<td class="p-2 text-center">
|
|
<button @click="editing = {{ e.id }}" class="btn-sm bg-cyber-border text-cyber-accent">Edit</button>
|
|
</td>
|
|
</template>
|
|
|
|
<!-- Mode edition -->
|
|
<template x-if="editing === {{ e.id }}">
|
|
<td colspan="8" class="p-2">
|
|
<form method="POST" action="/planning/{{ e.id }}/edit" class="flex gap-2 items-center flex-wrap">
|
|
<span class="font-mono text-xs text-gray-500">{{ e.week_code }}</span>
|
|
<select name="domain_code" class="text-xs py-1 px-2">
|
|
<option value="">-</option>
|
|
{% for d in all_domains %}<option value="{{ d.code }}" {% if d.code == e.domain_code %}selected{% endif %}>{{ d.name }}</option>{% endfor %}
|
|
</select>
|
|
<select name="env_scope" class="text-xs py-1 px-2">
|
|
{% for es in env_scopes %}<option value="{{ es }}" {% if es == e.env_scope %}selected{% endif %}>{{ es }}</option>{% endfor %}
|
|
</select>
|
|
<input type="number" name="cycle" value="{{ e.cycle or '' }}" placeholder="Cycle" class="text-xs py-1 px-2 w-16" min="1" max="4">
|
|
<select name="status" class="text-xs py-1 px-2">
|
|
{% for st in statuses %}<option value="{{ st }}" {% if st == e.status %}selected{% endif %}>{{ st }}</option>{% endfor %}
|
|
</select>
|
|
<input type="text" name="note" value="{{ e.note or '' }}" placeholder="Note" class="text-xs py-1 px-2 flex-1">
|
|
<button type="submit" class="btn-sm bg-cyber-accent text-black">OK</button>
|
|
<button type="button" @click="editing = null" class="btn-sm bg-cyber-border text-gray-400">X</button>
|
|
</form>
|
|
<form method="POST" action="/planning/{{ e.id }}/delete" class="inline ml-2">
|
|
<button type="submit" class="btn-sm bg-red-900/30 text-cyber-red" onclick="return confirm('Supprimer ?')">Suppr</button>
|
|
</form>
|
|
</td>
|
|
</template>
|
|
{% endif %}
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{% if perms.planning in ('edit', 'admin') %}
|
|
<!-- Ajouter une entree -->
|
|
<div class="card p-4 mt-4">
|
|
<h4 class="text-sm font-bold text-cyber-accent mb-3">Ajouter une entree</h4>
|
|
<form method="POST" action="/planning/add" class="flex gap-2 items-end flex-wrap">
|
|
<input type="hidden" name="year" value="{{ year }}">
|
|
<div>
|
|
<label class="text-xs text-gray-500">Semaine</label>
|
|
<input type="number" name="week_number" min="1" max="53" value="{{ default_week }}" class="text-xs py-1 px-2 w-16" required>
|
|
</div>
|
|
<div>
|
|
<label class="text-xs text-gray-500">Domaine</label>
|
|
<select name="domain_code" class="text-xs py-1 px-2">
|
|
<option value="">- (gel)</option>
|
|
{% for d in all_domains %}<option value="{{ d.code }}">{{ d.name }}</option>{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="text-xs text-gray-500">Env</label>
|
|
<select name="env_scope" class="text-xs py-1 px-2">
|
|
{% for es in env_scopes %}<option value="{{ es }}">{{ es }}</option>{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="text-xs text-gray-500">Cycle</label>
|
|
<input type="number" name="cycle" min="1" max="4" class="text-xs py-1 px-2 w-16">
|
|
</div>
|
|
<div>
|
|
<label class="text-xs text-gray-500">Statut</label>
|
|
<select name="status" class="text-xs py-1 px-2">
|
|
{% for st in statuses %}<option value="{{ st }}">{{ st }}</option>{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="flex-1">
|
|
<label class="text-xs text-gray-500">Note</label>
|
|
<input type="text" name="note" class="text-xs py-1 px-2 w-full">
|
|
</div>
|
|
<button type="submit" class="btn-primary px-4 py-1 text-sm">Ajouter</button>
|
|
</form>
|
|
</div>
|
|
{% endif %}
|
|
{% endblock %}
|