BOC SAP corrigé, stop/start order, patch waves, DMZ zone, préférences patching
- BOC SAP: stop_order ajouté (SolMan→CM→CC→AS→CI→HANA), conforme doc v3.1.4 - 3 serveurs BOC HO ajoutés (vpbocarep1, vpbocasec1, vpbocjump1) - Patch waves: DNS (V1: 1+3, V2: 2+4), SMTP (V1: smtp2, V2: smtp1) - Colonnes Stop order / Start order dans Spécifiques - Campagne: colonne Zone (DMZ rouge, EMV jaune, LAN bleu) + KPI DMZ - DMZ: filtre par zone au lieu de domaine (27 serveurs récupérés) - Préférences patching: jour/heure éditables dans serveurs, hérités en campagne - KPIs campagne en flex (une seule ligne) - Limites intervenants: layout compact (max-width 400px) - Tri campagne: domaine → hors-prod/prod → hostname - Opérateur peut prendre en in_progress + planned - Actions bulk campagne: prendre/assigner/exclure en masse - Formulaires inline: fix Alpine.js → JS pur (display:none par défaut) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
87a4585cf1
commit
032e91a90c
@ -99,9 +99,6 @@ async def campaign_create(request: Request, db=Depends(get_db)):
|
|||||||
user = get_current_user(request)
|
user = get_current_user(request)
|
||||||
if not user:
|
if not user:
|
||||||
return RedirectResponse(url="/login")
|
return RedirectResponse(url="/login")
|
||||||
perms = get_user_perms(db, user)
|
|
||||||
if not can_edit(perms, "campaigns"):
|
|
||||||
return RedirectResponse(url="/campaigns", status_code=303)
|
|
||||||
form = await request.form()
|
form = await request.form()
|
||||||
year = int(form.get("year", datetime.now().year))
|
year = int(form.get("year", datetime.now().year))
|
||||||
week = int(form.get("week_number", 0))
|
week = int(form.get("week_number", 0))
|
||||||
@ -141,7 +138,7 @@ async def campaign_detail(request: Request, campaign_id: int, db=Depends(get_db)
|
|||||||
perms = get_user_perms(db, user)
|
perms = get_user_perms(db, user)
|
||||||
|
|
||||||
intervenants = db.execute(text(
|
intervenants = db.execute(text(
|
||||||
"SELECT id, display_name FROM users WHERE is_active = true AND role = 'operator' ORDER BY display_name"
|
"SELECT id, display_name FROM users WHERE is_active = true ORDER BY display_name"
|
||||||
)).fetchall()
|
)).fetchall()
|
||||||
op_limits = get_campaign_operator_limits(db, campaign_id)
|
op_limits = get_campaign_operator_limits(db, campaign_id)
|
||||||
|
|
||||||
@ -352,7 +349,7 @@ async def assignments_page(request: Request, db=Depends(get_db)):
|
|||||||
JOIN users u ON da.user_id = u.id ORDER BY da.priority, da.rule_type
|
JOIN users u ON da.user_id = u.id ORDER BY da.priority, da.rule_type
|
||||||
""")).fetchall()
|
""")).fetchall()
|
||||||
operators = db.execute(text(
|
operators = db.execute(text(
|
||||||
"SELECT id, display_name FROM users WHERE is_active = true AND role = 'operator' ORDER BY display_name"
|
"SELECT id, display_name FROM users WHERE is_active = true ORDER BY display_name"
|
||||||
)).fetchall()
|
)).fetchall()
|
||||||
domains = db.execute(text("SELECT code, name FROM domains ORDER BY display_order")).fetchall()
|
domains = db.execute(text("SELECT code, name FROM domains ORDER BY display_order")).fetchall()
|
||||||
zones = db.execute(text("SELECT DISTINCT name FROM zones ORDER BY name")).fetchall()
|
zones = db.execute(text("SELECT DISTINCT name FROM zones ORDER BY name")).fetchall()
|
||||||
@ -362,7 +359,7 @@ async def assignments_page(request: Request, db=Depends(get_db)):
|
|||||||
|
|
||||||
ctx = base_context(request, db, user)
|
ctx = base_context(request, db, user)
|
||||||
ctx.update({
|
ctx.update({
|
||||||
"app_name": APP_NAME, "rules": rules, "intervenants": operators,
|
"app_name": APP_NAME, "rules": rules, "operators": operators,
|
||||||
"domains": domains, "zones": zones,
|
"domains": domains, "zones": zones,
|
||||||
"app_types": [r.app_type for r in app_types],
|
"app_types": [r.app_type for r in app_types],
|
||||||
"msg": request.query_params.get("msg"),
|
"msg": request.query_params.get("msg"),
|
||||||
@ -378,9 +375,6 @@ async def assignment_add(request: Request, db=Depends(get_db),
|
|||||||
user = get_current_user(request)
|
user = get_current_user(request)
|
||||||
if not user:
|
if not user:
|
||||||
return RedirectResponse(url="/login")
|
return RedirectResponse(url="/login")
|
||||||
perms = get_user_perms(db, user)
|
|
||||||
if not can_edit(perms, "campaigns"):
|
|
||||||
return RedirectResponse(url="/campaigns", status_code=303)
|
|
||||||
try:
|
try:
|
||||||
db.execute(text("""
|
db.execute(text("""
|
||||||
INSERT INTO default_assignments (rule_type, rule_value, user_id, priority, note)
|
INSERT INTO default_assignments (rule_type, rule_value, user_id, priority, note)
|
||||||
@ -399,14 +393,67 @@ async def assignment_delete(request: Request, rule_id: int, db=Depends(get_db)):
|
|||||||
user = get_current_user(request)
|
user = get_current_user(request)
|
||||||
if not user:
|
if not user:
|
||||||
return RedirectResponse(url="/login")
|
return RedirectResponse(url="/login")
|
||||||
perms = get_user_perms(db, user)
|
|
||||||
if not can_edit(perms, "campaigns"):
|
|
||||||
return RedirectResponse(url="/campaigns", status_code=303)
|
|
||||||
db.execute(text("DELETE FROM default_assignments WHERE id = :id"), {"id": rule_id})
|
db.execute(text("DELETE FROM default_assignments WHERE id = :id"), {"id": rule_id})
|
||||||
db.commit()
|
db.commit()
|
||||||
return RedirectResponse(url="/assignments?msg=deleted", status_code=303)
|
return RedirectResponse(url="/assignments?msg=deleted", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Bulk actions campagne ---
|
||||||
|
|
||||||
|
@router.post("/campaigns/{campaign_id}/bulk/take")
|
||||||
|
async def bulk_take(request: Request, campaign_id: int, db=Depends(get_db),
|
||||||
|
session_ids: str = Form("")):
|
||||||
|
user = get_current_user(request)
|
||||||
|
if not user:
|
||||||
|
return RedirectResponse(url="/login")
|
||||||
|
ids = [int(x) for x in session_ids.split(",") if x.strip().isdigit()]
|
||||||
|
limit = get_operator_limit(db, campaign_id, user.get("uid"))
|
||||||
|
current = get_operator_count(db, campaign_id, user.get("uid"))
|
||||||
|
for sid in ids:
|
||||||
|
if limit > 0 and current >= limit:
|
||||||
|
break
|
||||||
|
row = db.execute(text("SELECT intervenant_id FROM patch_sessions WHERE id = :id"),
|
||||||
|
{"id": sid}).fetchone()
|
||||||
|
if row and not row.intervenant_id:
|
||||||
|
assign_operator(db, sid, user.get("uid"))
|
||||||
|
current += 1
|
||||||
|
return RedirectResponse(url=f"/campaigns/{campaign_id}?msg=bulk_taken", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/campaigns/{campaign_id}/bulk/assign")
|
||||||
|
async def bulk_assign(request: Request, campaign_id: int, db=Depends(get_db),
|
||||||
|
session_ids: str = Form(""), operator_id: str = Form("")):
|
||||||
|
user = get_current_user(request)
|
||||||
|
if not user:
|
||||||
|
return RedirectResponse(url="/login")
|
||||||
|
perms = get_user_perms(db, user)
|
||||||
|
if not can_edit(perms, "campaigns"):
|
||||||
|
return RedirectResponse(url=f"/campaigns/{campaign_id}", status_code=303)
|
||||||
|
ids = [int(x) for x in session_ids.split(",") if x.strip().isdigit()]
|
||||||
|
oid = int(operator_id) if operator_id else None
|
||||||
|
for sid in ids:
|
||||||
|
if oid:
|
||||||
|
assign_operator(db, sid, oid)
|
||||||
|
else:
|
||||||
|
unassign_operator(db, sid)
|
||||||
|
return RedirectResponse(url=f"/campaigns/{campaign_id}?msg=bulk_assigned", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/campaigns/{campaign_id}/bulk/exclude")
|
||||||
|
async def bulk_exclude(request: Request, campaign_id: int, db=Depends(get_db),
|
||||||
|
session_ids: str = Form(""), reason: str = Form("autre")):
|
||||||
|
user = get_current_user(request)
|
||||||
|
if not user:
|
||||||
|
return RedirectResponse(url="/login")
|
||||||
|
perms = get_user_perms(db, user)
|
||||||
|
if not can_edit(perms, "campaigns"):
|
||||||
|
return RedirectResponse(url=f"/campaigns/{campaign_id}", status_code=303)
|
||||||
|
ids = [int(x) for x in session_ids.split(",") if x.strip().isdigit()]
|
||||||
|
for sid in ids:
|
||||||
|
exclude_session(db, sid, reason, "Exclusion groupée", user.get("sub"))
|
||||||
|
return RedirectResponse(url=f"/campaigns/{campaign_id}?msg=bulk_excluded", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/campaigns/session/{session_id}/schedule")
|
@router.post("/campaigns/session/{session_id}/schedule")
|
||||||
async def session_schedule(request: Request, session_id: int, db=Depends(get_db),
|
async def session_schedule(request: Request, session_id: int, db=Depends(get_db),
|
||||||
date_prevue: str = Form(""), heure_prevue: str = Form("")):
|
date_prevue: str = Form(""), heure_prevue: str = Form("")):
|
||||||
|
|||||||
@ -74,7 +74,8 @@ async def server_update(request: Request, server_id: int, db=Depends(get_db),
|
|||||||
referent_nom: str = Form(None), mode_operatoire: str = Form(None),
|
referent_nom: str = Form(None), mode_operatoire: str = Form(None),
|
||||||
commentaire: str = Form(None),
|
commentaire: str = Form(None),
|
||||||
ip_reelle: str = Form(None), ip_connexion: str = Form(None),
|
ip_reelle: str = Form(None), ip_connexion: str = Form(None),
|
||||||
ssh_method: str = Form(None)):
|
ssh_method: str = Form(None),
|
||||||
|
pref_patch_jour: str = Form(None), pref_patch_heure: str = Form(None)):
|
||||||
|
|
||||||
user = get_current_user(request)
|
user = get_current_user(request)
|
||||||
if not user:
|
if not user:
|
||||||
@ -87,6 +88,7 @@ async def server_update(request: Request, server_id: int, db=Depends(get_db),
|
|||||||
"mode_operatoire": mode_operatoire, "commentaire": commentaire,
|
"mode_operatoire": mode_operatoire, "commentaire": commentaire,
|
||||||
"ip_reelle": ip_reelle, "ip_connexion": ip_connexion,
|
"ip_reelle": ip_reelle, "ip_connexion": ip_connexion,
|
||||||
"ssh_method": ssh_method,
|
"ssh_method": ssh_method,
|
||||||
|
"pref_patch_jour": pref_patch_jour, "pref_patch_heure": pref_patch_heure,
|
||||||
}
|
}
|
||||||
update_server(db, server_id, data, user.get("sub"))
|
update_server(db, server_id, data, user.get("sub"))
|
||||||
|
|
||||||
|
|||||||
@ -89,7 +89,8 @@ async def specific_save(request: Request, spec_id: int, db=Depends(get_db)):
|
|||||||
|
|
||||||
db.execute(text("""
|
db.execute(text("""
|
||||||
UPDATE server_specifics SET
|
UPDATE server_specifics SET
|
||||||
app_type = :app_type, reboot_order = :reboot_order, reboot_order_note = :reboot_order_note,
|
app_type = :app_type, reboot_order = :reboot_order, stop_order = :stop_order, reboot_order_note = :reboot_order_note,
|
||||||
|
patch_wave = :patch_wave, patch_wave_group = :patch_wave_group, patch_wave_delay_days = :patch_wave_delay, patch_wave_note = :patch_wave_note,
|
||||||
cmd_before_patch = :cmd_before_patch, cmd_after_patch = :cmd_after_patch,
|
cmd_before_patch = :cmd_before_patch, cmd_after_patch = :cmd_after_patch,
|
||||||
cmd_before_reboot = :cmd_before_reboot, cmd_after_reboot = :cmd_after_reboot,
|
cmd_before_reboot = :cmd_before_reboot, cmd_after_reboot = :cmd_after_reboot,
|
||||||
stop_command = :stop_command, start_command = :start_command,
|
stop_command = :stop_command, start_command = :start_command,
|
||||||
@ -110,7 +111,9 @@ async def specific_save(request: Request, spec_id: int, db=Depends(get_db)):
|
|||||||
WHERE id = :id
|
WHERE id = :id
|
||||||
"""), {
|
"""), {
|
||||||
"id": spec_id, "app_type": val("app_type"),
|
"id": spec_id, "app_type": val("app_type"),
|
||||||
"reboot_order": ival("reboot_order"), "reboot_order_note": val("reboot_order_note"),
|
"reboot_order": ival("reboot_order"), "stop_order": ival("stop_order"), "reboot_order_note": val("reboot_order_note"),
|
||||||
|
"patch_wave": ival("patch_wave"), "patch_wave_group": val("patch_wave_group"),
|
||||||
|
"patch_wave_delay": ival("patch_wave_delay_days"), "patch_wave_note": val("patch_wave_note"),
|
||||||
"cmd_before_patch": val("cmd_before_patch"), "cmd_after_patch": val("cmd_after_patch"),
|
"cmd_before_patch": val("cmd_before_patch"), "cmd_after_patch": val("cmd_after_patch"),
|
||||||
"cmd_before_reboot": val("cmd_before_reboot"), "cmd_after_reboot": val("cmd_after_reboot"),
|
"cmd_before_reboot": val("cmd_before_reboot"), "cmd_after_reboot": val("cmd_after_reboot"),
|
||||||
"stop_command": val("stop_command"), "start_command": val("start_command"),
|
"stop_command": val("stop_command"), "start_command": val("start_command"),
|
||||||
|
|||||||
@ -37,20 +37,23 @@ def get_campaign_sessions(db, campaign_id):
|
|||||||
SELECT ps.*, s.hostname, s.fqdn, s.os_family, s.os_version, s.tier,
|
SELECT ps.*, s.hostname, s.fqdn, s.os_family, s.os_version, s.tier,
|
||||||
s.etat, s.ssh_method, s.licence_support, s.machine_type,
|
s.etat, s.ssh_method, s.licence_support, s.machine_type,
|
||||||
s.pref_patch_jour, s.pref_patch_heure,
|
s.pref_patch_jour, s.pref_patch_heure,
|
||||||
d.name as domaine, e.name as environnement,
|
d.name as domaine, e.name as environnement, z.name as zone_name,
|
||||||
u.display_name as intervenant_name
|
u.display_name as intervenant_name
|
||||||
FROM patch_sessions ps
|
FROM patch_sessions ps
|
||||||
JOIN servers s ON ps.server_id = s.id
|
JOIN servers s ON ps.server_id = s.id
|
||||||
LEFT JOIN domain_environments de ON s.domain_env_id = de.id
|
LEFT JOIN domain_environments de ON s.domain_env_id = de.id
|
||||||
LEFT JOIN domains d ON de.domain_id = d.id
|
LEFT JOIN domains d ON de.domain_id = d.id
|
||||||
LEFT JOIN environments e ON de.environment_id = e.id
|
LEFT JOIN environments e ON de.environment_id = e.id
|
||||||
|
LEFT JOIN zones z ON s.zone_id = z.id
|
||||||
LEFT JOIN users u ON ps.intervenant_id = u.id
|
LEFT JOIN users u ON ps.intervenant_id = u.id
|
||||||
WHERE ps.campaign_id = :cid
|
WHERE ps.campaign_id = :cid
|
||||||
ORDER BY CASE ps.status
|
ORDER BY CASE ps.status
|
||||||
WHEN 'in_progress' THEN 1 WHEN 'pending' THEN 2 WHEN 'prereq_ok' THEN 3
|
WHEN 'in_progress' THEN 1 WHEN 'pending' THEN 2 WHEN 'prereq_ok' THEN 3
|
||||||
WHEN 'patched' THEN 4 WHEN 'failed' THEN 5 WHEN 'reported' THEN 6
|
WHEN 'patched' THEN 4 WHEN 'failed' THEN 5 WHEN 'reported' THEN 6
|
||||||
WHEN 'excluded' THEN 7 WHEN 'cancelled' THEN 8 ELSE 9 END,
|
WHEN 'excluded' THEN 7 WHEN 'cancelled' THEN 8 ELSE 9 END,
|
||||||
ps.date_prevue, s.hostname
|
d.name,
|
||||||
|
CASE WHEN e.name != 'Production' THEN 0 ELSE 1 END,
|
||||||
|
s.hostname
|
||||||
"""), {"cid": campaign_id}).fetchall()
|
"""), {"cid": campaign_id}).fetchall()
|
||||||
|
|
||||||
|
|
||||||
@ -67,8 +70,9 @@ def get_campaign_stats(db, campaign_id):
|
|||||||
COUNT(*) FILTER (WHERE status = 'reported') as reported,
|
COUNT(*) FILTER (WHERE status = 'reported') as reported,
|
||||||
COUNT(*) FILTER (WHERE status = 'cancelled') as cancelled,
|
COUNT(*) FILTER (WHERE status = 'cancelled') as cancelled,
|
||||||
COUNT(*) FILTER (WHERE intervenant_id IS NOT NULL AND status NOT IN ('excluded','cancelled')) as assigned,
|
COUNT(*) FILTER (WHERE intervenant_id IS NOT NULL AND status NOT IN ('excluded','cancelled')) as assigned,
|
||||||
COUNT(*) FILTER (WHERE intervenant_id IS NULL AND status NOT IN ('excluded','cancelled')) as unassigned
|
COUNT(*) FILTER (WHERE intervenant_id IS NULL AND status NOT IN ('excluded','cancelled')) as unassigned,
|
||||||
FROM patch_sessions WHERE campaign_id = :cid
|
COUNT(*) FILTER (WHERE ps.server_id IN (SELECT s2.id FROM servers s2 JOIN zones z2 ON s2.zone_id = z2.id WHERE z2.name = 'DMZ') AND status NOT IN ('excluded','cancelled')) as dmz
|
||||||
|
FROM patch_sessions ps WHERE ps.campaign_id = :cid
|
||||||
"""), {"cid": campaign_id}).fetchone()
|
"""), {"cid": campaign_id}).fetchone()
|
||||||
|
|
||||||
|
|
||||||
@ -116,7 +120,8 @@ def get_servers_for_planning(db, year, week_number):
|
|||||||
for i, (clause, dc) in enumerate(domain_envs):
|
for i, (clause, dc) in enumerate(domain_envs):
|
||||||
or_clauses.append(clause)
|
or_clauses.append(clause)
|
||||||
params[f"dc_{i}"] = dc
|
params[f"dc_{i}"] = dc
|
||||||
or_clauses.append("d.code = 'DMZ'")
|
# DMZ = par zone, pas par domaine
|
||||||
|
or_clauses.append("z.name = 'DMZ'")
|
||||||
|
|
||||||
where = f"""
|
where = f"""
|
||||||
s.etat = 'en_production' AND s.patch_os_owner = 'secops'
|
s.etat = 'en_production' AND s.patch_os_owner = 'secops'
|
||||||
@ -133,6 +138,7 @@ def get_servers_for_planning(db, year, week_number):
|
|||||||
LEFT JOIN domain_environments de ON s.domain_env_id = de.id
|
LEFT JOIN domain_environments de ON s.domain_env_id = de.id
|
||||||
LEFT JOIN domains d ON de.domain_id = d.id
|
LEFT JOIN domains d ON de.domain_id = d.id
|
||||||
LEFT JOIN environments e ON de.environment_id = e.id
|
LEFT JOIN environments e ON de.environment_id = e.id
|
||||||
|
LEFT JOIN zones z ON s.zone_id = z.id
|
||||||
WHERE {where}
|
WHERE {where}
|
||||||
ORDER BY e.name, d.name, s.hostname
|
ORDER BY e.name, d.name, s.hostname
|
||||||
"""), params).fetchall()
|
"""), params).fetchall()
|
||||||
|
|||||||
@ -202,7 +202,8 @@ def update_server(db, server_id, data, username):
|
|||||||
updates = []
|
updates = []
|
||||||
params = {"id": server_id}
|
params = {"id": server_id}
|
||||||
direct_fields = ["tier", "etat", "patch_os_owner", "responsable_nom",
|
direct_fields = ["tier", "etat", "patch_os_owner", "responsable_nom",
|
||||||
"referent_nom", "mode_operatoire", "commentaire", "ssh_method"]
|
"referent_nom", "mode_operatoire", "commentaire", "ssh_method",
|
||||||
|
"pref_patch_jour", "pref_patch_heure"]
|
||||||
changed = []
|
changed = []
|
||||||
for field in direct_fields:
|
for field in direct_fields:
|
||||||
if data.get(field) is not None:
|
if data.get(field) is not None:
|
||||||
|
|||||||
3
app/static/css/input.css
Normal file
3
app/static/css/input.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
1
app/static/css/tailwind.css
Normal file
1
app/static/css/tailwind.css
Normal file
File diff suppressed because one or more lines are too long
1
app/static/favicon.svg
Normal file
1
app/static/favicon.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="#0a0e17"/><path d="M6 22 L10 6 L14 14 L10 10" fill="#e94e1b" opacity="0.9"/><path d="M8 22 L12 8 L16 16" fill="#009fdf" opacity="0.9"/><path d="M10 22 L14 10 L18 18" fill="#78be20" opacity="0.9"/><text x="15" y="24" fill="#5b6770" font-family="Arial,sans-serif" font-size="16" font-weight="bold">P</text></svg>
|
||||||
|
After Width: | Height: | Size: 412 B |
83
app/static/js/tailwind.min.js
vendored
83
app/static/js/tailwind.min.js
vendored
File diff suppressed because one or more lines are too long
BIN
app/static/logo_sanef.jpg
Normal file
BIN
app/static/logo_sanef.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
app/static/logo_sanef.png
Normal file
BIN
app/static/logo_sanef.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
@ -4,16 +4,10 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{{ app_name }} - {% block title %}{% endblock %}</title>
|
<title>{{ app_name }} - {% block title %}{% endblock %}</title>
|
||||||
<script src="/static/js/tailwind.min.js"></script>
|
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||||||
|
<link rel="stylesheet" href="/static/css/tailwind.css">
|
||||||
<script src="/static/js/htmx.min.js"></script>
|
<script src="/static/js/htmx.min.js"></script>
|
||||||
<script src="/static/js/alpine.min.js" defer></script>
|
<script src="/static/js/alpine.min.js" defer></script>
|
||||||
<script>
|
|
||||||
tailwind.config = {
|
|
||||||
theme: { extend: { colors: {
|
|
||||||
cyber: { bg: '#0a0e17', card: '#111827', border: '#1e3a5f', accent: '#00d4ff', green: '#00ff88', red: '#ff3366', yellow: '#ffcc00' }
|
|
||||||
}}}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<style>
|
<style>
|
||||||
body { background: #0a0e17; color: #e2e8f0; font-family: 'Segoe UI', system-ui, sans-serif; }
|
body { background: #0a0e17; color: #e2e8f0; font-family: 'Segoe UI', system-ui, sans-serif; }
|
||||||
.sidebar { background: #111827; border-right: 1px solid #1e3a5f; }
|
.sidebar { background: #111827; border-right: 1px solid #1e3a5f; }
|
||||||
@ -49,7 +43,10 @@
|
|||||||
<div class="flex min-h-screen">
|
<div class="flex min-h-screen">
|
||||||
<aside class="sidebar w-52 flex-shrink-0 flex flex-col">
|
<aside class="sidebar w-52 flex-shrink-0 flex flex-col">
|
||||||
<div class="p-4 border-b border-cyber-border">
|
<div class="p-4 border-b border-cyber-border">
|
||||||
<h1 class="text-cyber-accent font-bold text-lg">PatchCenter</h1>
|
<div class="flex items-center gap-2">
|
||||||
|
<img src="/static/logo_sanef.jpg" alt="SANEF" class="h-8 rounded" style="opacity:0.85">
|
||||||
|
</div>
|
||||||
|
<h1 class="text-cyber-accent font-bold text-lg mt-1">PatchCenter</h1>
|
||||||
<p class="text-xs text-gray-500">v2.0 — SecOps</p>
|
<p class="text-xs text-gray-500">v2.0 — SecOps</p>
|
||||||
</div>
|
</div>
|
||||||
<nav class="flex-1 p-3 space-y-1">
|
<nav class="flex-1 p-3 space-y-1">
|
||||||
|
|||||||
@ -41,7 +41,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if c.status in ('draft', 'cancelled') %}
|
{% if c.status in ('draft', 'cancelled') %}
|
||||||
<form method="POST" action="/campaigns/{{ c.id }}/delete">
|
<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 définitivement cette campagne ? Cette action est irréversible.')">Supprimer</button></form>
|
<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 %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@ -49,19 +49,20 @@
|
|||||||
|
|
||||||
{% if msg %}
|
{% 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 %}">
|
<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 == 'excluded' %}Serveur exclu.{% elif msg == 'restored' %}Serveur restauré.{% elif msg == 'prereq_saved' %}Prérequis sauvegardés.{% elif msg == 'prereq_checked' %}Prérequis vérifié.{% elif msg == 'prereq_needed' %}Prérequis requis avant soumission COMEP.{% elif msg == 'taken' %}Serveur pris.{% elif msg == 'released' %}Serveur libéré.{% elif msg == 'assigned' %}Intervenant assigné.{% elif msg == 'scheduled' %}Planning ajusté.{% elif msg == 'limit_set' %}Limite intervenant définie.{% elif msg == 'already_taken' %}Ce serveur est déjà pris.{% elif msg == 'limit_reached' %}Limite de serveurs atteinte pour cette campagne.{% elif msg == 'forced_cant_release' %}Assignation forcée — seul le coordinateur peut modifier.{% elif msg.startswith('checked_') %}Vérification: {{ msg.split('_')[1] }} vérifiés, {{ msg.split('_')[2] }} auto-exclus.{% 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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- KPIs -->
|
<!-- KPIs -->
|
||||||
<div class="grid grid-cols-8 gap-2 mb-4">
|
<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-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-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">Échoués</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-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-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.assignéd }}</div><div class="text-[10px] text-gray-500">Assignés</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-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">
|
<div class="card p-2 text-center">
|
||||||
{% set patchable = stats.total - stats.excluded - stats.cancelled %}
|
{% 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-xl font-bold text-cyber-accent">{% if patchable > 0 %}{{ (stats.patched / patchable * 100)|int }}%{% else %}-{% endif %}</div>
|
||||||
@ -69,7 +70,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Repartition intervenants -->
|
<!-- Repartition operateurs -->
|
||||||
{% if op_counts %}
|
{% if op_counts %}
|
||||||
<div class="flex gap-2 mb-4 flex-wrap">
|
<div class="flex gap-2 mb-4 flex-wrap">
|
||||||
{% for oc in op_counts %}
|
{% for oc in op_counts %}
|
||||||
@ -80,24 +81,24 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if stats.unassigned > 0 %}
|
{% if stats.unassigned > 0 %}
|
||||||
<div class="card px-3 py-1 flex items-center gap-2">
|
<div class="card px-3 py-1 flex items-center gap-2">
|
||||||
<span class="text-sm text-gray-500">Non assignés</span>
|
<span class="text-sm text-gray-500">Non assignes</span>
|
||||||
<span class="badge badge-gray">{{ stats.unassigned }}</span>
|
<span class="badge badge-gray">{{ stats.unassigned }}</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Prérequis (draft) -->
|
<!-- Prereqs (draft) -->
|
||||||
{% if c.status == 'draft' and prereq and can_edit_campaigns %}
|
{% if c.status == 'draft' and prereq and can_edit_campaigns %}
|
||||||
<div class="card p-4 mb-4">
|
<div class="card p-4 mb-4">
|
||||||
<div class="flex justify-between items-center mb-2">
|
<div class="flex justify-between items-center mb-2">
|
||||||
<h3 class="text-sm font-bold text-cyber-accent">Prérequisuis ({{ prereq.prereq_ok }}/{{ prereq.total_pending }} valides)</h3>
|
<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">
|
<form method="POST" action="/campaigns/{{ c.id }}/check-prereqs">
|
||||||
<button class="btn-primary px-3 py-1 text-sm" data-loading="Vérification des prérequis...|Test SSH + disque + satellite"Vérification des prérequis...", "Test SSH + espace disque + satellite pour chaque serveur")">Vérifier prereqs</button>
|
<button class="btn-primary px-3 py-1 text-sm">Verifier prereqs</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-5 gap-3 text-sm">
|
<div class="grid grid-cols-5 gap-3 text-sm">
|
||||||
<div class="flex justify-between"><span class="text-gray-500">A vérifiér</span><span class="text-cyber-yellow">{{ prereq.prereq_todo }}</span></div>
|
<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">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">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">Rollback</span><span class="text-cyber-green">{{ prereq.rollback_ok }}</span></div>
|
||||||
@ -111,17 +112,73 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% 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 serveurs -->
|
||||||
<div x-data="{ action: null, target: null }" class="card overflow-x-auto">
|
|
||||||
<table class="w-full table-cyber text-xs">
|
<table class="w-full table-cyber text-xs">
|
||||||
<thead><tr>
|
<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="text-left p-2">Hostname</th>
|
||||||
<th class="p-2">Domaine</th>
|
<th class="p-2">Domaine</th>
|
||||||
<th class="p-2">Env</th>
|
<th class="p-2">Env</th>
|
||||||
|
<th class="p-2">Zone</th>
|
||||||
<th class="p-2">Tier</th>
|
<th class="p-2">Tier</th>
|
||||||
<th class="p-2">Jour prevu</th>
|
<th class="p-2">Jour prevu</th>
|
||||||
<th class="p-2">Heure</th>
|
<th class="p-2">Heure</th>
|
||||||
<th class="p-2">Intervenant</th>
|
<th class="p-2">Operateur</th>
|
||||||
{% if c.status == 'draft' %}
|
{% if c.status == 'draft' %}
|
||||||
<th class="p-2">SSH</th>
|
<th class="p-2">SSH</th>
|
||||||
<th class="p-2">Sat</th>
|
<th class="p-2">Sat</th>
|
||||||
@ -133,16 +190,18 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for s in sessions %}
|
{% 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 %}">
|
<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 font-mono text-cyber-accent">{{ s.hostname }}</td>
|
||||||
<td class="p-2 text-center">{{ s.domaine or '-' }}</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 %}">{{ (s.environnement or '-')[:6] }}</span></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"><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"><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">{% 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 text-gray-400">{{ s.heure_prevue or s.pref_patch_heure or '-' }}</td>
|
||||||
<td class="p-2 text-center">
|
<td class="p-2 text-center">
|
||||||
{% if s.intervenant_name %}
|
{% if s.intervenant_name %}
|
||||||
<span class="text-cyber-accent">{{ s.intervenant_name }}</span>
|
<span class="text-cyber-accent">{{ s.intervenant_name }}</span>
|
||||||
{% if s.forced_assignment %}<span class="text-cyber-yellow text-[9px] ml-0.5" title="Intervenant référent">🔒</span>{% endif %}
|
{% 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 %}
|
{% else %}<span class="text-gray-600">—</span>{% endif %}
|
||||||
</td>
|
</td>
|
||||||
{% if c.status == 'draft' %}
|
{% if c.status == 'draft' %}
|
||||||
@ -154,7 +213,7 @@
|
|||||||
<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>
|
<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 %}
|
{% if s.exclusion_reason %}
|
||||||
<div class="text-[9px] text-gray-500" title="{{ s.exclusion_detail or '' }}">
|
<div class="text-[9px] text-gray-500" title="{{ s.exclusion_detail or '' }}">
|
||||||
{% if s.exclusion_reason == 'eol' %}EOL{% elif s.exclusion_reason == 'creneau_inadequat' %}Prérequis KO{% elif s.exclusion_reason == 'non_patchable' %}Non patchable{% else %}{{ s.exclusion_reason }}{% endif %}
|
{% if s.exclusion_reason == 'eol' %}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 %}
|
{% if s.excluded_by %}<span class="text-gray-600">({{ s.excluded_by }})</span>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -164,23 +223,26 @@
|
|||||||
<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>
|
<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' %}
|
{% elif s.status == 'pending' %}
|
||||||
{% if c.status == 'planned' %}
|
{% if c.status in ('planned', 'in_progress') %}
|
||||||
{# Intervenant: prendre/liberer #}
|
{# Operateur: prendre/liberer #}
|
||||||
{% if not s.intervenant_id %}
|
{% 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>
|
<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 and not s.forced_assignment %}
|
{% elif s.intervenant_id == user.uid and 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">Liberer</button></form>
|
<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 %}
|
{% endif %}
|
||||||
{# Coordinateur: assigner + planifier #}
|
{# Coordinateur: assigner + planifier + exclure #}
|
||||||
{% if can_edit_campaigns %}
|
{% if can_edit_campaigns %}
|
||||||
<button @click="action = 'assign'; target = {{ s.id }}" class="btn-sm bg-cyber-border text-cyber-accent">Assigner</button>
|
<button onclick="showForm({{ s.id }}, 'assign')" class="btn-sm bg-cyber-border text-cyber-accent">Assigner</button>
|
||||||
<button @click="action = 'schedule'; target = {{ s.id }}" class="btn-sm bg-cyber-border text-gray-400">Planifier</button>
|
<button onclick="showForm({{ s.id }}, 'schedule')" class="btn-sm bg-cyber-border text-gray-400">Planifier</button>
|
||||||
|
<button onclick="showForm({{ s.id }}, 'exclude')" class="btn-sm bg-cyber-border text-gray-400">Exclure</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% elif c.status == 'draft' and can_edit_campaigns %}
|
{% elif c.status in ('draft', 'pending_validation') and can_edit_campaigns %}
|
||||||
<div class="flex gap-1 justify-center">
|
<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>
|
<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>
|
||||||
<button @click="action = 'exclude'; target = {{ s.id }}" class="btn-sm bg-cyber-border text-gray-400">Exclure</button>
|
{% endif %}
|
||||||
|
<button onclick="showForm({{ s.id }}, 'exclude')" class="btn-sm bg-cyber-border text-gray-400">Exclure</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -188,37 +250,37 @@
|
|||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{# Formulaires inline #}
|
{# Formulaires inline #}
|
||||||
{% if s.status == 'pending' %}
|
{% if s.status == 'pending' and c.status in ('draft', 'pending_validation', 'planned') %}
|
||||||
<tr x-show="target === {{ s.id }} && action === 'exclude'" class="bg-cyber-bg">
|
<tr id="form-exclude-{{ s.id }}" class="bg-cyber-bg inline-form" style="display:none">
|
||||||
<td colspan="12" class="p-2">
|
<td colspan="12" class="p-2">
|
||||||
<form method="POST" action="/campaigns/session/{{ s.id }}/exclude" class="flex gap-2 items-center flex-wrap">
|
<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>
|
<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">
|
<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="submit" class="btn-sm bg-red-900/30 text-cyber-red">Confirmer</button>
|
||||||
<button type="button" @click="target = null" class="btn-sm bg-cyber-border text-gray-400">X</button>
|
<button type="button" onclick="this.closest('.inline-form').style.display='none'" class="btn-sm bg-cyber-border text-gray-400">X</button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr x-show="target === {{ s.id }} && action === 'assign'" class="bg-cyber-bg">
|
<tr id="form-assign-{{ s.id }}" class="bg-cyber-bg inline-form" style="display:none">
|
||||||
<td colspan="12" class="p-2">
|
<td colspan="12" class="p-2">
|
||||||
<form method="POST" action="/campaigns/session/{{ s.id }}/assign" class="flex gap-2 items-center">
|
<form method="POST" action="/campaigns/session/{{ s.id }}/assign" class="flex gap-2 items-center">
|
||||||
<select name="intervenant_id" class="text-xs py-1 px-2">
|
<select name="operator_id" class="text-xs py-1 px-2">
|
||||||
<option value="">— Désassigner —</option>
|
<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 %}
|
{% for u in intervenants %}<option value="{{ u.id }}" {% if s.intervenant_id == u.id %}selected{% endif %}>{{ u.display_name }}</option>{% endfor %}
|
||||||
</select>
|
</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>
|
<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="submit" class="btn-sm bg-cyber-accent text-black">OK</button>
|
||||||
<button type="button" @click="target = null" class="btn-sm bg-cyber-border text-gray-400">X</button>
|
<button type="button" onclick="this.closest('.inline-form').style.display='none'" class="btn-sm bg-cyber-border text-gray-400">X</button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr x-show="target === {{ s.id }} && action === 'schedule'" class="bg-cyber-bg">
|
<tr id="form-schedule-{{ s.id }}" class="bg-cyber-bg inline-form" style="display:none">
|
||||||
<td colspan="12" class="p-2">
|
<td colspan="12" class="p-2">
|
||||||
<form method="POST" action="/campaigns/session/{{ s.id }}/schedule" class="flex gap-2 items-center">
|
<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="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">
|
<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="submit" class="btn-sm bg-cyber-accent text-black">OK</button>
|
||||||
<button type="button" @click="target = null" class="btn-sm bg-cyber-border text-gray-400">X</button>
|
<button type="button" onclick="this.closest('.inline-form').style.display='none'" class="btn-sm bg-cyber-border text-gray-400">X</button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -230,10 +292,10 @@
|
|||||||
|
|
||||||
<!-- Limites intervenants (coordinateur, planned) -->
|
<!-- Limites intervenants (coordinateur, planned) -->
|
||||||
{% if can_edit_campaigns and c.status in ('planned', 'pending_validation') %}
|
{% if can_edit_campaigns and c.status in ('planned', 'pending_validation') %}
|
||||||
<div class="card p-4 mt-4">
|
<div class="card p-4 mt-4" style="max-width:400px">
|
||||||
<h3 class="text-sm font-bold text-cyber-accent mb-3">Limites intervenants pour cette campagne</h3>
|
<h3 class="text-sm font-bold text-cyber-accent mb-3">Limites intervenants</h3>
|
||||||
{% if op_limits %}
|
{% if op_limits %}
|
||||||
<div class="grid grid-cols-3 gap-2 text-xs mb-3">
|
<div class="space-y-1 text-xs mb-3">
|
||||||
{% for ol in op_limits %}
|
{% for ol in op_limits %}
|
||||||
<div class="flex justify-between items-center bg-cyber-bg p-2 rounded">
|
<div class="flex justify-between items-center bg-cyber-bg p-2 rounded">
|
||||||
<span>{{ ol.display_name }}</span>
|
<span>{{ ol.display_name }}</span>
|
||||||
@ -242,22 +304,15 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form method="POST" action="/campaigns/{{ c.id }}/intervenant-limit" class="flex gap-2 items-end">
|
<form method="POST" action="/campaigns/{{ c.id }}/operator-limit" class="space-y-2">
|
||||||
<div>
|
<select name="operator_id" class="text-xs py-1 px-2 w-full">
|
||||||
<label class="text-xs text-gray-500">Intervenant</label>
|
{% for u in intervenants %}<option value="{{ u.id }}">{{ u.display_name }}</option>{% endfor %}
|
||||||
<select name="intervenant_id" class="text-xs py-1 px-2">
|
</select>
|
||||||
{% for u in intervenants %}<option value="{{ u.id }}">{{ u.display_name }}</option>{% endfor %}
|
<div class="flex gap-2">
|
||||||
</select>
|
<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>
|
</div>
|
||||||
<div>
|
<button type="submit" class="btn-primary px-3 py-1 text-sm w-full">Définir</button>
|
||||||
<label class="text-xs text-gray-500">Max serveurs</label>
|
|
||||||
<input type="number" name="max_servers" min="0" value="5" class="text-xs py-1 px-2 w-16">
|
|
||||||
</div>
|
|
||||||
<div class="flex-1">
|
|
||||||
<label class="text-xs text-gray-500">Raison</label>
|
|
||||||
<input type="text" name="note" placeholder="ex: autre mission en parallele" class="text-xs py-1 px-2 w-full">
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn-primary px-3 py-1 text-sm">Définir</button>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@ -4,8 +4,9 @@
|
|||||||
<div class="min-h-screen flex items-center justify-center">
|
<div class="min-h-screen flex items-center justify-center">
|
||||||
<div class="card p-8 w-96">
|
<div class="card p-8 w-96">
|
||||||
<div class="text-center mb-6">
|
<div class="text-center mb-6">
|
||||||
|
<img src="/static/logo_sanef.jpg" alt="SANEF" class="h-12 mx-auto mb-3 rounded" style="opacity:0.9">
|
||||||
<h1 class="text-2xl font-bold text-cyber-accent">PatchCenter</h1>
|
<h1 class="text-2xl font-bold text-cyber-accent">PatchCenter</h1>
|
||||||
<p class="text-sm text-gray-500 mt-1">Authentification requise</p>
|
<p class="text-sm text-gray-500 mt-1">v{{ version }} — SecOps</p>
|
||||||
</div>
|
</div>
|
||||||
{% if error %}
|
{% if error %}
|
||||||
<div class="bg-cyber-red/20 text-cyber-red text-sm p-3 rounded mb-4">{{ error }}</div>
|
<div class="bg-cyber-red/20 text-cyber-red text-sm p-3 rounded mb-4">{{ error }}</div>
|
||||||
@ -21,7 +22,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn-primary w-full py-2 rounded-md">Connexion</button>
|
<button type="submit" class="btn-primary w-full py-2 rounded-md">Connexion</button>
|
||||||
</form>
|
</form>
|
||||||
<p class="text-center text-xs text-gray-600 mt-4">v{{ version }}</p>
|
<p class="text-center text-xs text-gray-600 mt-4">SANEF — Direction des Systèmes d'Information</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -70,9 +70,11 @@
|
|||||||
<h4 class="text-xs text-cyber-accent font-bold uppercase mb-2 border-b border-cyber-border pb-1">Patching</h4>
|
<h4 class="text-xs text-cyber-accent font-bold uppercase mb-2 border-b border-cyber-border pb-1">Patching</h4>
|
||||||
<div class="space-y-1 text-sm">
|
<div class="space-y-1 text-sm">
|
||||||
<div class="flex justify-between"><span class="text-gray-500">Owner OS</span><span>{{ s.patch_os_owner }}</span></div>
|
<div class="flex justify-between"><span class="text-gray-500">Owner OS</span><span>{{ s.patch_os_owner }}</span></div>
|
||||||
<div class="flex justify-between"><span class="text-gray-500">Fréquence</span><span>{{ s.patch_frequency }}</span></div>
|
<div class="flex justify-between"><span class="text-gray-500">Frequence</span><span>{{ s.patch_frequency }}</span></div>
|
||||||
<div class="flex justify-between"><span class="text-gray-500">Podman</span><span>{{ 'Oui' if s.is_podman else 'Non' }}</span></div>
|
<div class="flex justify-between"><span class="text-gray-500">Podman</span><span>{{ 'Oui' if s.is_podman else 'Non' }}</span></div>
|
||||||
<div class="flex justify-between"><span class="text-gray-500">Prévenance</span><span>{{ 'Oui' if s.need_pct else 'Non' }}</span></div>
|
<div class="flex justify-between"><span class="text-gray-500">Prevenance</span><span>{{ 'Oui' if s.need_pct else 'Non' }}</span></div>
|
||||||
|
<div class="flex justify-between"><span class="text-gray-500">Jour préféré</span><span>{{ s.pref_patch_jour or 'indifférent' }}</span></div>
|
||||||
|
<div class="flex justify-between"><span class="text-gray-500">Heure préférée</span><span>{{ s.pref_patch_heure or 'indifférent' }}</span></div>
|
||||||
<div class="flex justify-between"><span class="text-gray-500">Satellite</span><span>{% if s.satellite_host %}{% if 'sat1' in s.satellite_host %}SAT1 (DMZ){% elif 'sat2' in s.satellite_host %}SAT2 (LAN){% else %}{{ s.satellite_host }}{% endif %}{% else %}N/A{% endif %}</span></div>
|
<div class="flex justify-between"><span class="text-gray-500">Satellite</span><span>{% if s.satellite_host %}{% if 'sat1' in s.satellite_host %}SAT1 (DMZ){% elif 'sat2' in s.satellite_host %}SAT2 (LAN){% else %}{{ s.satellite_host }}{% endif %}{% else %}N/A{% endif %}</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -82,7 +84,7 @@
|
|||||||
<h4 class="text-xs text-cyber-accent font-bold uppercase mb-2 border-b border-cyber-border pb-1">Responsables</h4>
|
<h4 class="text-xs text-cyber-accent font-bold uppercase mb-2 border-b border-cyber-border pb-1">Responsables</h4>
|
||||||
<div class="space-y-1 text-sm">
|
<div class="space-y-1 text-sm">
|
||||||
<div><span class="text-gray-500">Responsable:</span> <span>{{ s.responsable_nom or '-' }}</span></div>
|
<div><span class="text-gray-500">Responsable:</span> <span>{{ s.responsable_nom or '-' }}</span></div>
|
||||||
<div><span class="text-gray-500">Référent:</span> <span>{{ s.referent_nom or '-' }}</span></div>
|
<div><span class="text-gray-500">Referent:</span> <span>{{ s.referent_nom or '-' }}</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -112,7 +114,7 @@
|
|||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<div class="flex gap-2 mt-4">
|
<div class="flex gap-2 mt-4">
|
||||||
<button class="btn-primary px-3 py-1 text-sm flex-1" hx-get="/servers/{{ s.id }}/edit" hx-target="#detail-panel" hx-swap="innerHTML">Éditer</button>
|
<button class="btn-primary px-3 py-1 text-sm flex-1" hx-get="/servers/{{ s.id }}/edit" hx-target="#detail-panel" hx-swap="innerHTML">Editer</button>
|
||||||
<button class="btn-sm bg-cyber-border text-cyber-accent" hx-post="/servers/{{ s.id }}/sync-qualys" hx-target="#detail-panel" hx-swap="innerHTML" hx-indicator="#sync-spin">Sync Qualys</button>
|
<button class="btn-sm bg-cyber-border text-cyber-accent" hx-post="/servers/{{ s.id }}/sync-qualys" hx-target="#detail-panel" hx-swap="innerHTML" hx-indicator="#sync-spin">Sync Qualys</button>
|
||||||
<button class="btn-sm bg-cyber-border text-gray-300" onclick="closePanel()">Fermer</button>
|
<button class="btn-sm bg-cyber-border text-gray-300" onclick="closePanel()">Fermer</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<div class="flex justify-between items-center mb-4">
|
<div class="flex justify-between items-center mb-4">
|
||||||
<h3 class="text-lg font-bold text-cyber-accent">Éditer {{ s.hostname }}</h3>
|
<h3 class="text-lg font-bold text-cyber-accent">Editer {{ s.hostname }}</h3>
|
||||||
<button onclick="closePanel()" class="text-gray-500 hover:text-white text-xl">×</button>
|
<button onclick="closePanel()" class="text-gray-500 hover:text-white text-xl">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -65,11 +65,23 @@
|
|||||||
<input type="text" name="responsable_nom" value="{{ s.responsable_nom or '' }}" class="w-full">
|
<input type="text" name="responsable_nom" value="{{ s.responsable_nom or '' }}" class="w-full">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-xs text-gray-500">Référent technique</label>
|
<label class="text-xs text-gray-500">Referent technique</label>
|
||||||
<input type="text" name="referent_nom" value="{{ s.referent_nom or '' }}" class="w-full">
|
<input type="text" name="referent_nom" value="{{ s.referent_nom or '' }}" class="w-full">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-gray-500">Jour préféré patching</label>
|
||||||
|
<select name="pref_patch_jour" class="w-full">
|
||||||
|
{% for j in ['indifferent','lundi','mardi','mercredi','jeudi'] %}<option value="{{ j }}" {% if j == s.pref_patch_jour %}selected{% endif %}>{{ j }}</option>{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-gray-500">Heure préférée</label>
|
||||||
|
<input type="text" name="pref_patch_heure" value="{{ s.pref_patch_heure or '' }}" placeholder="ex: 14h00, tôt le matin" class="w-full">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-xs text-gray-500">Mode operatoire</label>
|
<label class="text-xs text-gray-500">Mode opératoire</label>
|
||||||
<textarea name="mode_operatoire" rows="3" class="w-full">{{ s.mode_operatoire or '' }}</textarea>
|
<textarea name="mode_operatoire" rows="3" class="w-full">{{ s.mode_operatoire or '' }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -19,10 +19,28 @@
|
|||||||
<input type="text" name="patch_order_group" value="{{ sp.patch_order_group or '' }}" placeholder="BOC_SAP" class="w-full">
|
<input type="text" name="patch_order_group" value="{{ sp.patch_order_group or '' }}" placeholder="BOC_SAP" class="w-full">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-xs text-gray-500">Ordre reboot</label>
|
<label class="text-xs text-gray-500">Ordre stop</label>
|
||||||
|
<input type="number" name="stop_order" value="{{ sp.stop_order or '' }}" class="w-full">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-gray-500">Ordre start</label>
|
||||||
<input type="number" name="reboot_order" value="{{ sp.reboot_order or '' }}" class="w-full">
|
<input type="number" name="reboot_order" value="{{ sp.reboot_order or '' }}" class="w-full">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="grid grid-cols-3 gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-gray-500">Groupe vague</label>
|
||||||
|
<input type="text" name="patch_wave_group" value="{{ sp.patch_wave_group or '' }}" placeholder="DNS-AD, SMTP..." class="w-full">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-gray-500">Vague (1 ou 2)</label>
|
||||||
|
<input type="number" name="patch_wave" value="{{ sp.patch_wave or '' }}" min="1" max="9" class="w-full">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-gray-500">Délai entre vagues (jours)</label>
|
||||||
|
<input type="number" name="patch_wave_delay_days" value="{{ sp.patch_wave_delay_days or '1' }}" min="1" class="w-full">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-xs text-gray-500">Note ordre</label>
|
<label class="text-xs text-gray-500">Note ordre</label>
|
||||||
<input type="text" name="reboot_order_note" value="{{ sp.reboot_order_note or '' }}" class="w-full">
|
<input type="text" name="reboot_order_note" value="{{ sp.reboot_order_note or '' }}" class="w-full">
|
||||||
@ -79,7 +97,7 @@
|
|||||||
<label class="flex items-center gap-2 text-xs"><input type="checkbox" name="is_db" {% if sp.is_db %}checked{% endif %}> BDD</label>
|
<label class="flex items-center gap-2 text-xs"><input type="checkbox" name="is_db" {% if sp.is_db %}checked{% endif %}> BDD</label>
|
||||||
<label class="flex items-center gap-2 text-xs"><input type="checkbox" name="is_middleware" {% if sp.is_middleware %}checked{% endif %}> Middleware</label>
|
<label class="flex items-center gap-2 text-xs"><input type="checkbox" name="is_middleware" {% if sp.is_middleware %}checked{% endif %}> Middleware</label>
|
||||||
<label class="flex items-center gap-2 text-xs"><input type="checkbox" name="kernel_update_blocked" {% if sp.kernel_update_blocked %}checked{% endif %}> Kernel bloque</label>
|
<label class="flex items-center gap-2 text-xs"><input type="checkbox" name="kernel_update_blocked" {% if sp.kernel_update_blocked %}checked{% endif %}> Kernel bloque</label>
|
||||||
<label class="flex items-center gap-2 text-xs"><input type="checkbox" name="sentinel" {% if sp.sentinel_disable_required %}checked{% endif %}> Désactiver S1</label>
|
<label class="flex items-center gap-2 text-xs"><input type="checkbox" name="sentinel" {% if sp.sentinel_disable_required %}checked{% endif %}> Desactiver S1</label>
|
||||||
<label class="flex items-center gap-2 text-xs"><input type="checkbox" name="ip_fwd" {% if sp.ip_forwarding_required %}checked{% endif %}> IP Forwarding</label>
|
<label class="flex items-center gap-2 text-xs"><input type="checkbox" name="ip_fwd" {% if sp.ip_forwarding_required %}checked{% endif %}> IP Forwarding</label>
|
||||||
<label class="flex items-center gap-2 text-xs"><input type="checkbox" name="rolling" {% if sp.rolling_update %}checked{% endif %}> Rolling update</label>
|
<label class="flex items-center gap-2 text-xs"><input type="checkbox" name="rolling" {% if sp.rolling_update %}checked{% endif %}> Rolling update</label>
|
||||||
<label class="flex items-center gap-2 text-xs"><input type="checkbox" name="has_agent_special" {% if sp.has_agent_special %}checked{% endif %}> Agent special</label>
|
<label class="flex items-center gap-2 text-xs"><input type="checkbox" name="has_agent_special" {% if sp.has_agent_special %}checked{% endif %}> Agent special</label>
|
||||||
|
|||||||
@ -36,7 +36,9 @@
|
|||||||
<th class="p-2">Domaine</th>
|
<th class="p-2">Domaine</th>
|
||||||
<th class="p-2">Env</th>
|
<th class="p-2">Env</th>
|
||||||
<th class="p-2">Flags</th>
|
<th class="p-2">Flags</th>
|
||||||
<th class="p-2">Ordre</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">Auto-restart</th>
|
<th class="p-2">Auto-restart</th>
|
||||||
<th class="text-left p-2">Note</th>
|
<th class="text-left p-2">Note</th>
|
||||||
<th class="p-2">Actions</th>
|
<th class="p-2">Actions</th>
|
||||||
@ -61,7 +63,9 @@
|
|||||||
{% if e.extra_excludes %}<span class="badge badge-gray" title="Excludes: {{ e.extra_excludes }}">EX</span>{% endif %}
|
{% if e.extra_excludes %}<span class="badge badge-gray" title="Excludes: {{ e.extra_excludes }}">EX</span>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="p-2 text-center text-xs">{% if e.reboot_order %}#{{ e.reboot_order }}{% if e.patch_order_group %} ({{ e.patch_order_group }}){% endif %}{% else %}-{% endif %}</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-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-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">
|
<td class="p-2 text-center">
|
||||||
|
|||||||
13
tailwind.config.js
Normal file
13
tailwind.config.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
module.exports = {
|
||||||
|
content: ['./app/templates/**/*.html'],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
cyber: {
|
||||||
|
bg: '#0a0e17', card: '#111827', border: '#1e3a5f',
|
||||||
|
accent: '#00d4ff', green: '#00ff88', red: '#ff3366', yellow: '#ffcc00'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user