Quick Win delete, UI planning/specifics reorganises, accents retires

- Safe Patching: bouton supprimer campagne (admin only)
- Safe Patching: boutons nouvelle campagne et planning en haut
- Safe Patching: message suppression dans les notifications
- Planning: formulaire ajouter deplace apres le Gantt (compact)
- Planning: accents retires des messages flash
- Specifics: formulaire ajouter deplace en haut avant le tableau
- Specifics: colonne Wave retiree, colonnes Stop/Start renommees

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalid MOUTAOUAKIL 2026-04-06 15:44:22 +02:00
parent 49d5658475
commit 833c4fc3d2
5 changed files with 76 additions and 79 deletions

View File

@ -127,6 +127,21 @@ async def safe_patching_detail(request: Request, campaign_id: int, db=Depends(ge
return templates.TemplateResponse("safe_patching_detail.html", ctx)
@router.post("/safe-patching/{campaign_id}/delete")
async def safe_patching_delete(request: Request, campaign_id: int, db=Depends(get_db)):
user = get_current_user(request)
if not user:
return RedirectResponse(url="/login")
perms = get_user_perms(db, user)
if perms.get("campaigns") != "admin":
return RedirectResponse(url="/safe-patching", status_code=303)
db.execute(text("DELETE FROM campaign_operator_limits WHERE campaign_id = :cid"), {"cid": campaign_id})
db.execute(text("DELETE FROM patch_sessions WHERE campaign_id = :cid"), {"cid": campaign_id})
db.execute(text("DELETE FROM campaigns WHERE id = :cid"), {"cid": campaign_id})
db.commit()
return RedirectResponse(url="/safe-patching?msg=deleted", status_code=303)
@router.post("/safe-patching/{campaign_id}/check-prereqs")
async def safe_patching_check_prereqs(request: Request, campaign_id: int, db=Depends(get_db),
branch: str = Form("hprod")):

View File

@ -19,7 +19,7 @@
{% 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 %}
{% if msg == 'add' %}Entree ajoutee.{% elif msg == 'edit' %}Entree modifiee.{% elif msg == 'delete' %}Entree supprimee.{% elif msg == 'duplicate' %}Planning duplique avec succes.{% elif msg == 'exists' %}L'annee cible contient deja des entrees. Supprimez-les d'abord.{% elif msg == 'err_week' %}Numero de semaine invalide (1-53).{% elif msg == 'err_domain' %}Domaine requis pour une entree ouverte.{% elif msg == 'err_past' %}Impossible d'ajouter dans le passe (semaine deja ecoulee).{% elif msg == 'err_past_wed' %}Semaine en cours : ajout possible uniquement lundi et mardi (MEP urgente).{% endif %}
</div>
{% endif %}
@ -94,6 +94,29 @@
</table>
</div>
{% if perms.planning in ('edit', 'admin') %}
<div class="card p-3 mt-4 mb-4">
<form method="POST" action="/planning/add" class="flex gap-2 items-end flex-wrap">
<input type="hidden" name="year" value="{{ year }}">
<span class="text-xs text-cyber-accent font-bold">Ajouter :</span>
<input type="number" name="week_number" min="1" max="53" value="{{ default_week }}" class="text-xs py-1 px-2 w-14" required placeholder="Sem">
<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>
<select name="env_scope" class="text-xs py-1 px-2">
{% for es in env_scopes %}<option value="{{ es }}">{{ es }}</option>{% endfor %}
</select>
<input type="number" name="cycle" min="1" max="4" class="text-xs py-1 px-2 w-14" placeholder="Cycle">
<select name="status" class="text-xs py-1 px-2">
{% for st in statuses %}<option value="{{ st }}">{{ st }}</option>{% endfor %}
</select>
<input type="text" name="note" class="text-xs py-1 px-2 flex-1" placeholder="Note">
<button type="submit" class="btn-primary px-3 py-1 text-sm">Ajouter</button>
</form>
</div>
{% endif %}
<!-- Detail par cycle -->
<div class="grid grid-cols-3 gap-4 mt-6">
{% for cycle_num in [1, 2, 3] %}
@ -203,45 +226,4 @@
</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 %}

View File

@ -1,12 +1,22 @@
{% extends 'base.html' %}
{% block title %}Safe Patching{% endblock %}
{% block content %}
<h2 class="text-xl font-bold text-cyber-accent mb-4">Safe Patching — Quick Win</h2>
<p class="text-xs text-gray-500 mb-4">Patching sans interruption de service : exclut tout ce qui nécessite un reboot ou un restart de service.</p>
<div class="flex justify-between items-center mb-4">
<div>
<h2 class="text-xl font-bold text-cyber-accent">Safe Patching — Quick Win</h2>
<p class="text-xs text-gray-500 mt-1">Patching sans interruption de service : exclut tout ce qui nécessite un reboot ou un restart.</p>
</div>
<div class="flex gap-2">
{% if can_create %}
<button onclick="document.getElementById('create-form').style.display = document.getElementById('create-form').style.display === 'none' ? 'block' : 'none'" class="btn-primary px-4 py-2 text-sm">Nouvelle campagne</button>
{% endif %}
<a href="/planning" class="btn-sm bg-cyber-border text-cyber-accent px-4 py-2">Planning</a>
</div>
</div>
{% if msg %}
<div class="mb-3 p-2 rounded text-sm bg-red-900/30 text-cyber-red">
{% if msg == 'error' %}Erreur à la création (semaine déjà existante ?).{% endif %}
{% if msg == 'error' %}Erreur à la création (semaine déjà existante ?).{% elif msg == 'deleted' %}Campagne supprimée.{% endif %}
</div>
{% endif %}
@ -30,14 +40,10 @@
</div>
{% endif %}
<!-- Créer Quick Win -->
<!-- Formulaire création -->
{% if can_create %}
<div x-data="{ show: false }" class="card p-5">
<div class="flex justify-between items-center">
<h3 class="text-sm font-bold text-cyber-accent">Nouvelle campagne Quick Win</h3>
<button @click="show = !show" class="btn-sm bg-cyber-border text-cyber-accent" x-text="show ? 'Masquer' : 'Créer'"></button>
</div>
<div x-show="show" class="mt-4">
<div id="create-form" class="card p-5 mb-4" style="display:none">
<h3 class="text-sm font-bold text-cyber-accent mb-3">Nouvelle campagne Quick Win</h3>
<form method="POST" action="/safe-patching/create" class="space-y-3">
<div class="grid grid-cols-3 gap-3">
<div>
@ -74,6 +80,5 @@
<button type="submit" class="btn-primary px-6 py-2 text-sm">Créer la campagne Quick Win</button>
</form>
</div>
</div>
{% endif %}
{% endblock %}

View File

@ -11,6 +11,12 @@
<span class="badge {% if c.status == 'draft' %}badge-gray{% elif c.status == 'in_progress' %}badge-yellow{% elif c.status == 'completed' %}badge-green{% else %}badge-red{% endif %}">{{ c.status }}</span>
</div>
</div>
{% set p = perms if perms is defined else request.state.perms %}
{% if p.campaigns == 'admin' %}
<form method="POST" action="/safe-patching/{{ c.id }}/delete">
<button class="btn-sm bg-red-900/50 text-cyber-red px-4 py-2" onclick="return confirm('SUPPRIMER définitivement cette campagne Quick Win ?')">Supprimer</button>
</form>
{% endif %}
</div>
{% if msg %}

View File

@ -24,6 +24,19 @@
</form>
</div>
<!-- Ajouter -->
<div class="card p-3 mb-4">
<form method="POST" action="/specifics/add" class="flex gap-3 items-end">
<span class="text-xs text-cyber-accent font-bold">Ajouter :</span>
<input type="text" name="hostname" required placeholder="vpinfaweb1" class="text-xs py-1 px-2 w-44 font-mono">
<select name="app_type" class="text-xs py-1 px-2">
<option value="">— Type —</option>
{% for t in app_types %}<option value="{{ t }}">{{ t }}</option>{% endfor %}
</select>
<button type="submit" class="btn-primary px-3 py-1 text-sm">Ajouter</button>
</form>
</div>
<!-- Panel édition -->
<div id="edit-panel" class="card mb-4 p-5" style="display:none"></div>
@ -36,9 +49,8 @@
<th class="p-2">Domaine</th>
<th class="p-2">Env</th>
<th class="p-2">Flags</th>
<th class="p-2">Stop order</th>
<th class="p-2">Start order</th>
<th class="p-2">Wave</th>
<th class="p-2">Stop</th>
<th class="p-2">Start</th>
<th class="p-2">Auto-restart</th>
<th class="text-left p-2">Note</th>
<th class="p-2">Actions</th>
@ -65,7 +77,6 @@
</td>
<td class="p-2 text-center text-xs">{% if e.stop_order %}#{{ e.stop_order }}{% else %}-{% endif %}</td>
<td class="p-2 text-center text-xs">{% if e.reboot_order %}#{{ e.reboot_order }}{% if e.patch_order_group %} <span class="text-gray-600">({{ e.patch_order_group }})</span>{% endif %}{% else %}-{% endif %}</td>
<td class="p-2 text-center text-xs">{% if e.patch_wave %}<span class="badge badge-blue">V{{ e.patch_wave }}</span> <span class="text-gray-500">{{ e.patch_wave_group or "" }}</span>{% else %}-{% endif %}</td>
<td class="p-2 text-center"><span class="badge {% if e.auto_restart %}badge-green{% else %}badge-red{% endif %}">{{ 'Oui' if e.auto_restart else 'Non' }}</span></td>
<td class="p-2 text-xs text-gray-400" style="max-width:300px">{{ (e.note or '')[:80] }}{% if e.note and e.note|length > 80 %}...{% endif %}</td>
<td class="p-2 text-center">
@ -81,26 +92,4 @@
<!-- Panel edition -->
<!-- Ajouter -->
<!-- Note Wave -->
<div class="card p-3 mt-4 text-xs text-gray-500">
<strong class="text-cyber-accent">Waves :</strong> Les serveurs d'un même groupe (DNS, SMTP, HAProxy...) ne doivent pas être patchés le même jour. V1 = première vague (jour J), V2 = deuxième vague (J + délai). Le délai entre vagues est configurable (défaut : 1 jour). Exemple : DNS V1 (dns1+dns3 lundi) → DNS V2 (dns2+dns4 mardi).
</div>
<div class="card p-4 mt-4">
<h4 class="text-sm font-bold text-cyber-accent mb-3">Ajouter un serveur specifique</h4>
<form method="POST" action="/specifics/add" class="flex gap-3 items-end">
<div>
<label class="text-xs text-gray-500">Hostname</label>
<input type="text" name="hostname" required placeholder="vpinfaweb1" class="text-xs py-1 px-2 w-44">
</div>
<div>
<label class="text-xs text-gray-500">Type application</label>
<select name="app_type" class="text-xs py-1 px-2">
<option value="">-</option>
{% for t in app_types %}<option value="{{ t }}">{{ t }}</option>{% endfor %}
</select>
</div>
<button type="submit" class="btn-primary px-4 py-1 text-sm">Ajouter</button>
</form>
</div>
{% endblock %}