From 2ab2ceabba149f02f81ded673cc2b30c1b09c127 Mon Sep 17 00:00:00 2001 From: Admin MPCZ Date: Fri, 17 Apr 2026 12:10:45 +0000 Subject: [PATCH] Historique patching : filtres OS/zone/domaine/intervenant + colonnes table --- app/routers/patch_history.py | 95 ++++++++++++++----- app/templates/patch_history.html | 35 +++++-- .../2026-04-17_patch_history_intervenant.sql | 10 ++ tools/import_plan_patching_xlsx.py | 21 ++-- 4 files changed, 122 insertions(+), 39 deletions(-) create mode 100644 deploy/migrations/2026-04-17_patch_history_intervenant.sql diff --git a/app/routers/patch_history.py b/app/routers/patch_history.py index 78a313c..f643d7c 100644 --- a/app/routers/patch_history.py +++ b/app/routers/patch_history.py @@ -14,6 +14,8 @@ templates = Jinja2Templates(directory="app/templates") async def patch_history_page(request: Request, db=Depends(get_db), year: int = Query(None), week: int = Query(None), hostname: str = Query(None), source: str = Query(None), + os_family: str = Query(None), zone: str = Query(None), + domain: str = Query(None), intervenant: str = Query(None), page: int = Query(1)): user = get_current_user(request) if not user: @@ -87,6 +89,25 @@ async def patch_history_page(request: Request, db=Depends(get_db), ) u GROUP BY week_num ORDER BY week_num """), {"y": year}).fetchall() + # Listes pour les filtres (selon annee courante) + filter_opts = {} + filter_opts["os"] = [r.os for r in db.execute(text(""" + SELECT DISTINCT s.os_family as os FROM servers s + WHERE s.os_family IS NOT NULL AND s.os_family <> '' + ORDER BY 1 + """)).fetchall()] + filter_opts["zones"] = [r.zone for r in db.execute(text(""" + SELECT DISTINCT z.name as zone FROM zones z ORDER BY 1 + """)).fetchall()] + filter_opts["domains"] = [r.dom for r in db.execute(text(""" + SELECT DISTINCT d.name as dom FROM domains d ORDER BY 1 + """)).fetchall()] + filter_opts["intervenants"] = [r.interv for r in db.execute(text(""" + SELECT DISTINCT intervenant_name as interv FROM patch_history + WHERE intervenant_name IS NOT NULL AND intervenant_name <> '' + ORDER BY 1 + """)).fetchall()] + where_ph = ["EXTRACT(YEAR FROM ph.date_patch)=:y"] where_qw = ["qr.year=:y", "qe.status='patched'"] params = {"y": year, "limit": per_page, "offset": offset} @@ -99,6 +120,22 @@ async def patch_history_page(request: Request, db=Depends(get_db), where_ph.append("s.hostname ILIKE :h") where_qw.append("s.hostname ILIKE :h") params["h"] = f"%{hostname}%" + if os_family: + where_ph.append("s.os_family=:os") + where_qw.append("s.os_family=:os") + params["os"] = os_family + if zone: + where_ph.append("z.name=:zn") + where_qw.append("z.name=:zn") + params["zn"] = zone + if domain: + where_ph.append("d.name=:dm") + where_qw.append("d.name=:dm") + params["dm"] = domain + if intervenant: + where_ph.append("ph.intervenant_name=:iv") + where_qw.append("1=0") # quickwin n'a pas ce champ + params["iv"] = intervenant if source == "import": where_ph.append("ph.campaign_id IS NULL") elif source == "standard": @@ -107,24 +144,29 @@ async def patch_history_page(request: Request, db=Depends(get_db), wc_ph = " AND ".join(where_ph) wc_qw = " AND ".join(where_qw) - skip_qw = source in ("import", "standard") + skip_qw = source in ("import", "standard") or bool(intervenant) skip_ph = source == "quickwin" + ph_joins = """ + JOIN servers s ON ph.server_id=s.id + LEFT JOIN zones z ON s.zone_id=z.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 campaigns c ON ph.campaign_id=c.id + """ + qw_joins = """ + JOIN quickwin_runs qr ON qe.run_id=qr.id + JOIN servers s ON qe.server_id=s.id + LEFT JOIN zones z ON s.zone_id=z.id + LEFT JOIN domain_environments de ON s.domain_env_id=de.id + LEFT JOIN domains d ON de.domain_id=d.id + """ + count_parts = [] if not skip_ph: - count_parts.append(f""" - SELECT COUNT(*) FROM patch_history ph - JOIN servers s ON ph.server_id=s.id - LEFT JOIN campaigns c ON ph.campaign_id=c.id - WHERE {wc_ph} - """) + count_parts.append(f"SELECT COUNT(*) FROM patch_history ph {ph_joins} WHERE {wc_ph}") if not skip_qw: - count_parts.append(f""" - SELECT COUNT(*) FROM quickwin_entries qe - JOIN quickwin_runs qr ON qe.run_id=qr.id - JOIN servers s ON qe.server_id=s.id - WHERE {wc_qw} - """) + count_parts.append(f"SELECT COUNT(*) FROM quickwin_entries qe {qw_joins} WHERE {wc_qw}") count_sql = " + ".join(f"({p})" for p in count_parts) if count_parts else "0" total_filtered = db.execute(text(f"SELECT {count_sql}"), params).scalar() @@ -132,34 +174,33 @@ async def patch_history_page(request: Request, db=Depends(get_db), if not skip_ph: union_parts.append(f""" SELECT s.id as sid, s.hostname, s.os_family, s.etat, - ph.date_patch, ph.status, ph.notes, - z.name as zone, + ph.date_patch, ph.status, ph.notes, ph.intervenant_name, + z.name as zone, d.name as domain_name, CASE WHEN ph.campaign_id IS NULL THEN 'import' ELSE COALESCE(c.campaign_type, 'standard') END as source_type, c.id as campaign_id, c.label as campaign_label, NULL::int as run_id, NULL::text as run_label - FROM patch_history ph - JOIN servers s ON ph.server_id=s.id - LEFT JOIN zones z ON s.zone_id=z.id - LEFT JOIN campaigns c ON ph.campaign_id=c.id + FROM patch_history ph {ph_joins} WHERE {wc_ph} """) if not skip_qw: union_parts.append(f""" SELECT s.id as sid, s.hostname, s.os_family, s.etat, qe.patch_date as date_patch, qe.status, qe.notes, - z.name as zone, + NULL::text as intervenant_name, + z.name as zone, d.name as domain_name, 'quickwin' as source_type, NULL::int as campaign_id, NULL::text as campaign_label, qr.id as run_id, qr.label as run_label - FROM quickwin_entries qe - JOIN quickwin_runs qr ON qe.run_id=qr.id - JOIN servers s ON qe.server_id=s.id - LEFT JOIN zones z ON s.zone_id=z.id + FROM quickwin_entries qe {qw_joins} WHERE {wc_qw} """) if not union_parts: - union_parts.append("SELECT NULL::int as sid, NULL as hostname, NULL as os_family, NULL as etat, NULL::timestamptz as date_patch, NULL as status, NULL as notes, NULL as zone, NULL as source_type, NULL::int as campaign_id, NULL as campaign_label, NULL::int as run_id, NULL as run_label WHERE 1=0") + union_parts.append("""SELECT NULL::int as sid, NULL as hostname, NULL as os_family, NULL as etat, + NULL::timestamptz as date_patch, NULL as status, NULL as notes, NULL as intervenant_name, + NULL as zone, NULL as domain_name, NULL as source_type, + NULL::int as campaign_id, NULL as campaign_label, NULL::int as run_id, NULL as run_label + WHERE 1=0""") union_sql = " UNION ALL ".join(union_parts) rows = db.execute(text(f""" @@ -180,6 +221,8 @@ async def patch_history_page(request: Request, db=Depends(get_db), "request": request, "user": user, "app_name": APP_NAME, "kpis": kpis, "by_week": by_week, "by_source": by_source, "rows": rows, "year": year, "week": week, "hostname": hostname, - "source": source, "page": page, "per_page": per_page, + "source": source, "os_family": os_family, "zone": zone, + "domain": domain, "intervenant": intervenant, + "filter_opts": filter_opts, "page": page, "per_page": per_page, "total_filtered": total_filtered, "years": [y.y for y in years], }) diff --git a/app/templates/patch_history.html b/app/templates/patch_history.html index 8f0ea85..df925e9 100644 --- a/app/templates/patch_history.html +++ b/app/templates/patch_history.html @@ -78,10 +78,26 @@ - + + + + + Reset {{ total_filtered }} résultat{{ 's' if total_filtered != 1 }} @@ -95,9 +111,11 @@ Hostname OS Zone + Domaine État Date - Semaine + Sem. + Intervenant Source Status Notes @@ -108,9 +126,11 @@ {{ r.hostname }} {{ (r.os_family or '-')[:6] }} {{ r.zone or '-' }} + {{ (r.domain_name or '-')[:10] }} {{ (r.etat or '-')[:6] }} {{ r.date_patch.strftime('%Y-%m-%d %H:%M') if r.date_patch else '-' }} {% if r.date_patch %}S{{ r.date_patch.strftime('%V') }}{% else %}-{% endif %} + {{ r.intervenant_name or '-' }} {% if r.source_type == 'import' %}xlsx {% elif r.source_type == 'standard' %}{{ r.campaign_label or 'Campagne' }} @@ -118,11 +138,11 @@ {% else %}{{ r.source_type or '?' }}{% endif %} {{ r.status }} - {{ (r.notes or '-')[:50] }} + {{ (r.notes or '-')[:40] }} {% endfor %} {% if not rows %} - Aucun event de patching pour ce filtre + Aucun event de patching pour ce filtre {% endif %} @@ -131,9 +151,10 @@ {% if total_filtered > per_page %}
- {% if page > 1 %}← Précédent{% endif %} + {% set qs = 'year=' ~ year ~ ('&week=' ~ week if week else '') ~ ('&source=' ~ source if source else '') ~ ('&os_family=' ~ os_family if os_family else '') ~ ('&zone=' ~ zone if zone else '') ~ ('&domain=' ~ domain if domain else '') ~ ('&intervenant=' ~ intervenant if intervenant else '') ~ ('&hostname=' ~ hostname if hostname else '') %} + {% if page > 1 %}← Précédent{% endif %} Page {{ page }} / {{ ((total_filtered - 1) // per_page) + 1 }} - {% if page * per_page < total_filtered %}Suivant →{% endif %} + {% if page * per_page < total_filtered %}Suivant →{% endif %}
{% endif %} {% endblock %} diff --git a/deploy/migrations/2026-04-17_patch_history_intervenant.sql b/deploy/migrations/2026-04-17_patch_history_intervenant.sql new file mode 100644 index 0000000..7c3dc59 --- /dev/null +++ b/deploy/migrations/2026-04-17_patch_history_intervenant.sql @@ -0,0 +1,10 @@ +-- Migration 2026-04-17 : ajout colonne intervenant_name a patch_history +-- pour stocker le nom d'intervenant libre provenant du xlsx (ex "Khalid", "Thierno") +-- sans FK users (car ne correspond pas forcement a un user patchcenter) + +BEGIN; + +ALTER TABLE patch_history ADD COLUMN IF NOT EXISTS intervenant_name varchar(100); +CREATE INDEX IF NOT EXISTS idx_ph_intervenant_name ON patch_history (intervenant_name); + +COMMIT; diff --git a/tools/import_plan_patching_xlsx.py b/tools/import_plan_patching_xlsx.py index f5fca1d..de0b29c 100644 --- a/tools/import_plan_patching_xlsx.py +++ b/tools/import_plan_patching_xlsx.py @@ -119,7 +119,7 @@ def collect_events(wb, hosts): stats = {"histo_2025_s1": 0, "histo_2025_s2": 0, "weekly": 0, "no_server": 0, "weekly_no_color": 0} - # --- Histo-2025 : col L (12) date S1, col M (13) flag S1, col O (15) date S2, col P (16) flag S2 + # --- Histo-2025 : col B (2) Intervenant, col L (12) date S1, col M (13) flag S1, col O (15) date S2, col P (16) flag S2 if "Histo-2025" in wb.sheetnames: ws = wb["Histo-2025"] for row_idx in range(2, ws.max_row + 1): @@ -131,12 +131,16 @@ def collect_events(wb, hosts): stats["no_server"] += 1 continue + interv = ws.cell(row=row_idx, column=2).value + interv = str(interv).strip() if interv else None + date_s1 = parse_date_cell(ws.cell(row=row_idx, column=12).value) flag_s1 = ws.cell(row=row_idx, column=13).value if flag_s1 and isinstance(flag_s1, int) and flag_s1 >= 1: dt = date_s1 or datetime(2025, 6, 30, 0, 0) events.append({"sid": sid, "dt": dt, "status": "ok", - "notes": f"Histo-2025 S1 (x{flag_s1})"}) + "notes": f"Histo-2025 S1 (x{flag_s1})", + "interv": interv}) stats["histo_2025_s1"] += 1 date_s2 = parse_date_cell(ws.cell(row=row_idx, column=15).value) @@ -144,7 +148,8 @@ def collect_events(wb, hosts): if flag_s2 and isinstance(flag_s2, int) and flag_s2 >= 1: dt = date_s2 or datetime(2025, 12, 31, 0, 0) events.append({"sid": sid, "dt": dt, "status": "ok", - "notes": f"Histo-2025 S2 (x{flag_s2})"}) + "notes": f"Histo-2025 S2 (x{flag_s2})", + "interv": interv}) stats["histo_2025_s2"] += 1 # --- Weekly sheets S02..S52 : nom colore VERT = patche (2026) @@ -170,6 +175,9 @@ def collect_events(wb, hosts): stats["no_server"] += 1 continue + interv = ws.cell(row=row_idx, column=2).value + interv = str(interv).strip() if interv else None + # col N (14) = Date, col O (15) = Heure date_val = ws.cell(row=row_idx, column=14).value hour_val = ws.cell(row=row_idx, column=15).value @@ -180,7 +188,8 @@ def collect_events(wb, hosts): # sinon : heure = 00:00 par defaut (deja dans dt_base) events.append({"sid": sid, "dt": dt_base, "status": "ok", - "notes": f"Semaine {wk:02d} 2026"}) + "notes": f"Semaine {wk:02d} 2026", + "interv": interv}) stats["weekly"] += 1 return events, stats @@ -235,8 +244,8 @@ def main(): skipped += 1 continue conn.execute(text(""" - INSERT INTO patch_history (server_id, date_patch, status, notes) - VALUES (:sid, :dt, :status, :notes) + INSERT INTO patch_history (server_id, date_patch, status, notes, intervenant_name) + VALUES (:sid, :dt, :status, :notes, :interv) """), ev) inserted += 1