"""Router campagnes — creation depuis planning + gestion exclusions""" from datetime import datetime from fastapi import APIRouter, Request, Depends, Query, Form from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.templating import Jinja2Templates from sqlalchemy import text from ..dependencies import get_db, get_current_user from ..services.campaign_service import ( list_campaigns, get_campaign, get_campaign_sessions, get_campaign_stats, create_campaign_from_planning, get_servers_for_planning, update_campaign_status, exclude_session, restore_session, validate_prereq, get_prereq_stats, can_plan_campaign, bulk_auto_exclude_failed_prereqs, ) from ..services.prereq_service import check_prereqs_campaign, check_single_prereq from ..config import APP_NAME router = APIRouter() templates = Jinja2Templates(directory="app/templates") EXCLUSION_REASONS = [ ("eol", "Fin de vie (EOL)"), ("creneau_inadequat", "Creneau non adequat"), ("intervention_non_secops", "Intervention non-SecOps prevue"), ("report_cycle", "Report au cycle suivant"), ("non_patchable", "Serveur non patchable"), ("autre", "Autre"), ] @router.get("/campaigns", response_class=HTMLResponse) async def campaigns_list(request: Request, db=Depends(get_db), year: int = Query(None), status: str = Query(None)): user = get_current_user(request) if not user: return RedirectResponse(url="/login") if not year: year = datetime.now().year campaigns = list_campaigns(db, year=year, status=status) # Semaines planifiees pour cette annee (pour le formulaire de creation) now = datetime.now() current_week = now.isocalendar()[1] planned_weeks = db.execute(text(""" SELECT DISTINCT pp.week_number, pp.week_code, pp.week_start, pp.week_end, string_agg(DISTINCT d.name || ' (' || pp.env_scope || ')', ', ' ORDER BY d.name || ' (' || pp.env_scope || ')') as scope FROM patch_planning pp LEFT JOIN domains d ON pp.domain_code = d.code WHERE pp.year = :y AND pp.status = 'open' AND pp.domain_code IS NOT NULL AND pp.week_number >= :cw GROUP BY pp.week_number, pp.week_code, pp.week_start, pp.week_end ORDER BY pp.week_number """), {"y": year, "cw": current_week}).fetchall() return templates.TemplateResponse("campaigns.html", { "request": request, "user": user, "app_name": APP_NAME, "campaigns": campaigns, "year": year, "status_filter": status, "planned_weeks": planned_weeks, }) @router.get("/campaigns/preview", response_class=HTMLResponse) async def campaign_preview(request: Request, db=Depends(get_db), year: int = Query(...), week: int = Query(...)): """HTMX: preview des serveurs pour une semaine du planning""" user = get_current_user(request) if not user: return HTMLResponse("

Non autorise

") servers, planning = get_servers_for_planning(db, year, week) scope = ", ".join(set(f"{p.domain_name} ({p.env_scope})" for p in planning if p.domain_code)) return templates.TemplateResponse("partials/campaign_preview.html", { "request": request, "servers": servers, "scope": scope, "week": week, "year": year, "count": len(servers), }) @router.post("/campaigns/create") async def campaign_create(request: Request, db=Depends(get_db)): user = get_current_user(request) if not user: return RedirectResponse(url="/login") form = await request.form() year = int(form.get("year", datetime.now().year)) week = int(form.get("week_number", 0)) label = form.get("label", f"Patch S{week:02d} {year}") # Serveurs exclus (checkboxes non cochees) excluded = [] for key in form.keys(): if key.startswith("exclude_"): sid = int(key.replace("exclude_", "")) excluded.append(sid) cid = create_campaign_from_planning(db, year, week, label, user.get("uid"), excluded) if not cid: return RedirectResponse(url=f"/campaigns?year={year}&msg=no_servers", status_code=303) return RedirectResponse(url=f"/campaigns/{cid}", status_code=303) @router.get("/campaigns/{campaign_id}", response_class=HTMLResponse) async def campaign_detail(request: Request, campaign_id: int, db=Depends(get_db)): user = get_current_user(request) if not user: return RedirectResponse(url="/login") campaign = get_campaign(db, campaign_id) if not campaign: return RedirectResponse(url="/campaigns") sessions = get_campaign_sessions(db, campaign_id) stats = get_campaign_stats(db, campaign_id) prereq = get_prereq_stats(db, campaign_id) can_plan = can_plan_campaign(db, campaign_id) return templates.TemplateResponse("campaign_detail.html", { "request": request, "user": user, "app_name": APP_NAME, "c": campaign, "sessions": sessions, "stats": stats, "prereq": prereq, "can_plan": can_plan, "exclusion_reasons": EXCLUSION_REASONS, "msg": request.query_params.get("msg"), }) @router.post("/campaigns/{campaign_id}/status") async def campaign_status_change(request: Request, campaign_id: int, db=Depends(get_db), new_status: str = Form(...)): user = get_current_user(request) if not user: return RedirectResponse(url="/login") # Bloquer planned si prereqs non valides if new_status == "planned" and not can_plan_campaign(db, campaign_id): return RedirectResponse(url=f"/campaigns/{campaign_id}?msg=prereq_needed", status_code=303) update_campaign_status(db, campaign_id, new_status) return RedirectResponse(url=f"/campaigns/{campaign_id}", status_code=303) @router.post("/campaigns/session/{session_id}/prereq") async def session_prereq(request: Request, session_id: int, db=Depends(get_db), prereq_ssh: str = Form(...), prereq_satellite: str = Form(...), rollback_method: str = Form(""), rollback_justif: str = Form("")): user = get_current_user(request) if not user: return RedirectResponse(url="/login") validate_prereq(db, session_id, prereq_ssh, prereq_satellite, rollback_method or None, rollback_justif, user.get("sub")) row = db.execute(text("SELECT campaign_id FROM patch_sessions WHERE id = :id"), {"id": session_id}).fetchone() cid = row.campaign_id if row else 0 return RedirectResponse(url=f"/campaigns/{cid}?msg=prereq_saved#row-{session_id}", status_code=303) @router.post("/campaigns/{campaign_id}/auto-exclude-failed") async def auto_exclude_failed(request: Request, campaign_id: int, db=Depends(get_db)): user = get_current_user(request) if not user: return RedirectResponse(url="/login") count = bulk_auto_exclude_failed_prereqs(db, campaign_id, user.get("sub")) return RedirectResponse(url=f"/campaigns/{campaign_id}?msg=auto_excluded_{count}", status_code=303) @router.post("/campaigns/{campaign_id}/check-prereqs") async def campaign_check_prereqs(request: Request, campaign_id: int, db=Depends(get_db)): """Lance la verification automatique des prereqs pour toute la campagne""" user = get_current_user(request) if not user: return RedirectResponse(url="/login") checked, auto_excluded = check_prereqs_campaign(db, campaign_id) return RedirectResponse( url=f"/campaigns/{campaign_id}?msg=checked_{checked}_{auto_excluded}", status_code=303 ) @router.post("/campaigns/session/{session_id}/check-prereq") async def session_check_prereq(request: Request, session_id: int, db=Depends(get_db)): """Lance la verification prereq pour un seul serveur""" user = get_current_user(request) if not user: return RedirectResponse(url="/login") check_single_prereq(db, session_id) row = db.execute(text("SELECT campaign_id FROM patch_sessions WHERE id = :id"), {"id": session_id}).fetchone() cid = row.campaign_id if row else 0 return RedirectResponse(url=f"/campaigns/{cid}?msg=prereq_checked#row-{session_id}", status_code=303) @router.post("/campaigns/session/{session_id}/exclude") async def session_exclude(request: Request, session_id: int, db=Depends(get_db), reason: str = Form(...), detail: str = Form("")): user = get_current_user(request) if not user: return RedirectResponse(url="/login") exclude_session(db, session_id, reason, detail, user.get("sub")) # Retrouver campaign_id row = db.execute(text("SELECT campaign_id FROM patch_sessions WHERE id = :id"), {"id": session_id}).fetchone() cid = row.campaign_id if row else 0 return RedirectResponse(url=f"/campaigns/{cid}?msg=excluded#row-{session_id}", status_code=303) @router.post("/campaigns/session/{session_id}/restore") async def session_restore(request: Request, session_id: int, db=Depends(get_db)): user = get_current_user(request) if not user: return RedirectResponse(url="/login") restore_session(db, session_id) row = db.execute(text("SELECT campaign_id FROM patch_sessions WHERE id = :id"), {"id": session_id}).fetchone() cid = row.campaign_id if row else 0 return RedirectResponse(url=f"/campaigns/{cid}?msg=restored#row-{session_id}", status_code=303)