From 5fedfb5f80f34e509d370ea4bda2e349f032189a Mon Sep 17 00:00:00 2001 From: Admin MPCZ Date: Fri, 17 Apr 2026 23:39:11 +0000 Subject: [PATCH] Add page Tour de garde SecOps : import xlsx + table + vue hebdo + competences --- app/main.py | 3 +- app/routers/duty.py | 57 +++++++ app/templates/base.html | 1 + app/templates/duty.html | 125 +++++++++++++++ deploy/Tour de garde secops_2026.xlsx | Bin 0 -> 30749 bytes deploy/migrations/2026-04-18_secops_duty.sql | 32 ++++ tools/import_tour_de_garde_xlsx.py | 159 +++++++++++++++++++ 7 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 app/routers/duty.py create mode 100644 app/templates/duty.html create mode 100644 deploy/Tour de garde secops_2026.xlsx create mode 100644 deploy/migrations/2026-04-18_secops_duty.sql create mode 100644 tools/import_tour_de_garde_xlsx.py diff --git a/app/main.py b/app/main.py index 68e4f1c..43ca935 100644 --- a/app/main.py +++ b/app/main.py @@ -12,7 +12,7 @@ from starlette.middleware.base import BaseHTTPMiddleware from .config import APP_NAME, APP_VERSION from .dependencies import get_current_user, get_user_perms from .database import SessionLocal, SessionLocalDemo -from .routers import auth, dashboard, servers, settings, users, campaigns, planning, specifics, audit, contacts, qualys, qualys_tags, quickwin, referentiel, patching, applications, patch_history +from .routers import auth, dashboard, servers, settings, users, campaigns, planning, specifics, audit, contacts, qualys, qualys_tags, quickwin, referentiel, patching, applications, patch_history, duty class PermissionsMiddleware(BaseHTTPMiddleware): @@ -71,6 +71,7 @@ app.include_router(quickwin.router) app.include_router(referentiel.router) app.include_router(patching.router) app.include_router(patch_history.router) +app.include_router(duty.router) app.include_router(applications.router) diff --git a/app/routers/duty.py b/app/routers/duty.py new file mode 100644 index 0000000..9f0796b --- /dev/null +++ b/app/routers/duty.py @@ -0,0 +1,57 @@ +"""Router Tour de garde SecOps""" +from fastapi import APIRouter, Request, Depends, Query +from fastapi.responses import HTMLResponse, RedirectResponse +from fastapi.templating import Jinja2Templates +from sqlalchemy import text +from ..dependencies import get_db, get_current_user +from ..config import APP_NAME + +router = APIRouter() +templates = Jinja2Templates(directory="app/templates") + + +@router.get("/duty", response_class=HTMLResponse) +async def duty_page(request: Request, db=Depends(get_db), + year: str = Query("")): + user = get_current_user(request) + if not user: + return RedirectResponse(url="/login") + + from datetime import datetime + year = int(year) if year and year.isdigit() else datetime.now().year + current_week = datetime.now().isocalendar()[1] + current_year = datetime.now().year + + rows = db.execute(text(""" + SELECT * FROM secops_duty + WHERE year = :y + ORDER BY week_number + """), {"y": year}).fetchall() + + years = db.execute(text(""" + SELECT DISTINCT year FROM secops_duty ORDER BY year DESC + """)).fetchall() + + # Competences (hardcoded pour l'instant, pourra etre en DB plus tard) + competences = [ + {"nom": "Ayoub", "s1": True, "commvault": False, "m365": True, "symantec": True}, + {"nom": "Mouaad", "s1": True, "commvault": False, "m365": True, "symantec": True}, + {"nom": "Khalid", "s1": True, "commvault": False, "m365": True, "symantec": True}, + {"nom": "Thierno", "s1": True, "commvault": True, "m365": True, "symantec": True}, + {"nom": "Paul", "s1": True, "commvault": True, "m365": True, "symantec": True}, + ] + + # Stats : qui a le plus de gardes + stats = {} + for r in rows: + for field in ["tdg_s1", "tdg_symantec", "tdg_m365", "tdg_dmz"]: + name = getattr(r, field, None) + if name: + stats[name] = stats.get(name, 0) + 1 + + return templates.TemplateResponse("duty.html", { + "request": request, "user": user, "app_name": APP_NAME, + "rows": rows, "year": year, "years": [y.year for y in years], + "current_week": current_week, "current_year": current_year, + "competences": competences, "stats": stats, + }) diff --git a/app/templates/base.html b/app/templates/base.html index 74570c1..ac32699 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -90,6 +90,7 @@ {% if p.servers in ('edit','admin') or p.campaigns in ('edit','admin') or p.quickwin in ('edit','admin') %}Config exclusions{% endif %} {% if p.campaigns in ('edit','admin') or p.quickwin in ('edit','admin') %}Validations{% endif %} Historique +Tour de garde {# Quickwin sous-groupe #} {% if p.campaigns or p.quickwin %} diff --git a/app/templates/duty.html b/app/templates/duty.html new file mode 100644 index 0000000..b24e811 --- /dev/null +++ b/app/templates/duty.html @@ -0,0 +1,125 @@ +{% extends 'base.html' %} +{% block title %}Tour de garde SecOps{% endblock %} +{% block content %} +
+
+

Tour de garde SecOps

+

Planning hebdomadaire des astreintes et responsabilites.

+
+
+ {% for y in years %}{{ y }}{% endfor %} +
+
+ + +{% for r in rows %} +{% if r.week_number == current_week and year == current_year %} +
+
+

Cette semaine — {{ r.week_code }}

+ {{ r.week_start.strftime('%d/%m') if r.week_start else '' }} → {{ r.week_end.strftime('%d/%m/%Y') if r.week_end else '' }} +
+ {% if r.absences %}

Absences : {{ r.absences }}

{% endif %} +
+
+
{{ r.tdg_s1 or '-' }}
+
Sentinel One
+
+
+
{{ r.tdg_symantec or '-' }}
+
Symantec
+
+
+
{{ r.tdg_m365 or '-' }}
+
M365
+
+
+
{{ r.tdg_commvault or '-' }}
+
Commvault
+
+
+
{{ r.tdg_dmz or '-' }}
+
DMZ
+
+
+
{{ r.tdg_meteo or '-' }}
+
Meteo
+
+
+
{{ r.tdg_incident_majeur or '-' }}
+
Incident Maj.
+
+
+
+{% endif %} +{% endfor %} + + +
+

Matrice competences

+ + + + + + + + + + + {% for c in competences %} + + + + + + + + + {% endfor %} + +
NomS1CommvaultM365SymantecGardes {{ year }}
{{ c.nom }}{% if c.s1 %}{% else %}{% endif %}{% if c.commvault %}{% else %}{% endif %}{% if c.m365 %}{% else %}{% endif %}{% if c.symantec %}{% else %}{% endif %}{{ stats.get(c.nom, 0) }}
+
+ + +
+ + + + + + + + + + + + + + + + + {% for r in rows %} + + + + + + + + + + + + + + + {% endfor %} + +
Sem.DatesAbsencesS1SymantecM365CommvaultMeteoDMZSafeNetQuarant.Inc. Maj.
{{ r.week_code }}{{ r.week_start.strftime('%d/%m') if r.week_start else '' }} → {{ r.week_end.strftime('%d/%m') if r.week_end else '' }}{{ r.absences or '' }}{{ r.tdg_s1 or '-' }}{{ r.tdg_symantec or '-' }}{{ r.tdg_m365 or '-' }}{{ r.tdg_commvault or '-' }}{{ r.tdg_meteo or '-' }}{{ r.tdg_dmz or '-' }}{{ r.tdg_safenet or '-' }}{{ r.tdg_quarantaine or '-' }}{{ r.tdg_incident_majeur or '-' }}
+
+ + +{% endblock %} diff --git a/deploy/Tour de garde secops_2026.xlsx b/deploy/Tour de garde secops_2026.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..6a2bb3bd76baddbfb3a523637999875aa485d01f GIT binary patch literal 30749 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 I+&=pM0D<;aK>z>% literal 0 HcmV?d00001 diff --git a/deploy/migrations/2026-04-18_secops_duty.sql b/deploy/migrations/2026-04-18_secops_duty.sql new file mode 100644 index 0000000..0dc5e7d --- /dev/null +++ b/deploy/migrations/2026-04-18_secops_duty.sql @@ -0,0 +1,32 @@ +-- Migration 2026-04-18 : table tour de garde SecOps +BEGIN; + +CREATE TABLE IF NOT EXISTS secops_duty ( + id serial PRIMARY KEY, + year smallint NOT NULL, + week_number smallint NOT NULL, + week_code varchar(5) NOT NULL, + week_start date, + week_end date, + absences text, + tdg_s1 varchar(50), + tdg_symantec varchar(50), + tdg_m365 varchar(50), + tdg_commvault varchar(50), + tdg_meteo varchar(50), + tdg_dmz varchar(50), + tdg_safenet varchar(50), + tdg_quarantaine varchar(50), + tdg_securisation varchar(50), + tdg_incident_majeur varchar(50), + tdg_incident_critique varchar(50), + emails_dest varchar(100), + created_at timestamptz DEFAULT now() +); + +CREATE UNIQUE INDEX IF NOT EXISTS secops_duty_year_week_uniq ON secops_duty (year, week_number); +CREATE INDEX IF NOT EXISTS secops_duty_week_idx ON secops_duty (year, week_number); + +COMMENT ON TABLE secops_duty IS 'Tour de garde SecOps hebdomadaire (source: Tour de garde secops_2026.xlsx)'; + +COMMIT; diff --git a/tools/import_tour_de_garde_xlsx.py b/tools/import_tour_de_garde_xlsx.py new file mode 100644 index 0000000..2d04e33 --- /dev/null +++ b/tools/import_tour_de_garde_xlsx.py @@ -0,0 +1,159 @@ +"""Import tour de garde SecOps depuis Tour de garde secops_2026.xlsx. + +Lit la feuille 'Tour de garde', UPSERT dans secops_duty. + +Usage: + python tools/import_tour_de_garde_xlsx.py [xlsx] [--truncate] +""" +import os +import sys +import re +import glob +from pathlib import Path +from datetime import date, datetime, timedelta + +import openpyxl +from sqlalchemy import create_engine, text + +ROOT = Path(__file__).resolve().parent.parent +DATABASE_URL = (os.getenv("DATABASE_URL_DEMO") + or os.getenv("DATABASE_URL") + or "postgresql://patchcenter:PatchCenter2026!@localhost:5432/patchcenter_db") + + +def parse_dates(c_val): + if not c_val: + return None, None + s = str(c_val).strip() + m = re.search(r"(\d{2})/(\d{2})/(\d{4}).*?(\d{2})/(\d{2})/(\d{4})", s) + if m: + d1 = date(int(m.group(3)), int(m.group(2)), int(m.group(1))) + d2 = date(int(m.group(6)), int(m.group(5)), int(m.group(4))) + return d1, d2 + return None, None + + +def s(val): + if val is None: + return None + t = str(val).strip() + return t or None + + +def find_xlsx(): + for p in [ + ROOT / "deploy" / "Tour de garde secops_2026.xlsx", + ROOT / "deploy" / "Tour_de_garde_secops_2026.xlsx", + ]: + if p.exists(): + return str(p) + hits = glob.glob(str(ROOT / "deploy" / "*our*garde*.xlsx")) + return hits[0] if hits else None + + +def parse_tour_de_garde(xlsx_path): + wb = openpyxl.load_workbook(xlsx_path, data_only=True) + ws_name = next((n for n in wb.sheetnames if "garde" in n.lower()), None) + if not ws_name: + raise SystemExit(f"[ERR] Sheet 'Tour de garde' introuvable. Sheets: {wb.sheetnames}") + ws = wb[ws_name] + + rows = [] + for i, row in enumerate(ws.iter_rows(values_only=True)): + if i == 0: + continue + week_code = row[0] + if not week_code or not str(week_code).strip().startswith("S"): + continue + + wc = str(week_code).strip() + m = re.match(r"S(\d+)", wc) + if not m: + continue + week_num = int(m.group(1)) + + d1, d2 = parse_dates(row[2]) + year = d1.year if d1 and d1.month > 6 else (d2.year if d2 else 2026) + if d1 and d1.month == 12 and week_num <= 1: + year = d2.year if d2 else d1.year + 1 + + rows.append({ + "year": year, + "week_number": week_num, + "week_code": wc, + "week_start": d1, + "week_end": d2, + "absences": s(row[1]), + "tdg_s1": s(row[3]), + "tdg_symantec": s(row[4]), + "tdg_m365": s(row[5]), + "emails_dest": s(row[6]), + "tdg_commvault": s(row[7]), + "tdg_meteo": s(row[8]), + "tdg_dmz": s(row[11]) if len(row) > 11 else None, + "tdg_safenet": s(row[12]) if len(row) > 12 else None, + "tdg_quarantaine": s(row[13]) if len(row) > 13 else None, + "tdg_securisation": s(row[14]) if len(row) > 14 else None, + "tdg_incident_majeur": s(row[16]) if len(row) > 16 else None, + "tdg_incident_critique": s(row[17]) if len(row) > 17 else None, + }) + return rows + + +SQL_UPSERT = text(""" + INSERT INTO secops_duty + (year, week_number, week_code, week_start, week_end, absences, + tdg_s1, tdg_symantec, tdg_m365, tdg_commvault, tdg_meteo, + tdg_dmz, tdg_safenet, tdg_quarantaine, tdg_securisation, + tdg_incident_majeur, tdg_incident_critique, emails_dest) + VALUES + (:year, :week_number, :week_code, :week_start, :week_end, :absences, + :tdg_s1, :tdg_symantec, :tdg_m365, :tdg_commvault, :tdg_meteo, + :tdg_dmz, :tdg_safenet, :tdg_quarantaine, :tdg_securisation, + :tdg_incident_majeur, :tdg_incident_critique, :emails_dest) + ON CONFLICT (year, week_number) DO UPDATE SET + week_code = EXCLUDED.week_code, + week_start = EXCLUDED.week_start, + week_end = EXCLUDED.week_end, + absences = EXCLUDED.absences, + tdg_s1 = EXCLUDED.tdg_s1, + tdg_symantec = EXCLUDED.tdg_symantec, + tdg_m365 = EXCLUDED.tdg_m365, + tdg_commvault = EXCLUDED.tdg_commvault, + tdg_meteo = EXCLUDED.tdg_meteo, + tdg_dmz = EXCLUDED.tdg_dmz, + tdg_safenet = EXCLUDED.tdg_safenet, + tdg_quarantaine = EXCLUDED.tdg_quarantaine, + tdg_securisation = EXCLUDED.tdg_securisation, + tdg_incident_majeur = EXCLUDED.tdg_incident_majeur, + tdg_incident_critique = EXCLUDED.tdg_incident_critique, + emails_dest = EXCLUDED.emails_dest +""") + + +def main(): + xlsx = sys.argv[1] if len(sys.argv) > 1 else find_xlsx() + if not xlsx or not os.path.exists(xlsx): + print("[ERR] Fichier Tour de garde introuvable. Place-le dans deploy/") + sys.exit(1) + + print(f"[INFO] Fichier: {xlsx}") + rows = parse_tour_de_garde(xlsx) + print(f"[INFO] Semaines parsees: {len(rows)}") + + engine = create_engine(DATABASE_URL) + print(f"[INFO] DB: {DATABASE_URL.rsplit('@', 1)[-1]}") + + truncate = "--truncate" in sys.argv + with engine.begin() as conn: + if truncate: + conn.execute(text("TRUNCATE TABLE secops_duty RESTART IDENTITY")) + print("[INFO] TRUNCATE secops_duty") + for r in rows: + conn.execute(SQL_UPSERT, r) + + print(f"[OK] UPSERT: {len(rows)} semaines") + + +if __name__ == "__main__": + main()