diff --git a/app/routers/audit_full.py b/app/routers/audit_full.py index d3e8eed..215552a 100644 --- a/app/routers/audit_full.py +++ b/app/routers/audit_full.py @@ -224,6 +224,112 @@ async def audit_full_import(request: Request, db=Depends(get_db), ) +@router.get("/audit-full/patching", response_class=HTMLResponse) +async def audit_full_patching(request: Request, db=Depends(get_db)): + user = get_current_user(request) + if not user: + return RedirectResponse(url="/login") + + year = int(request.query_params.get("year", "2026")) + search = request.query_params.get("q", "").strip() + domain = request.query_params.get("domain", "") + page = int(request.query_params.get("page", "1")) + sort = request.query_params.get("sort", "hostname") + sort_dir = request.query_params.get("dir", "asc") + per_page = 30 + + yr_count = "patch_count_2026" if year == 2026 else "patch_count_2025" + yr_weeks = "patch_weeks_2026" if year == 2026 else "patch_weeks_2025" + + kpis = db.execute(text( + f"SELECT COUNT(*) as total," + f" COUNT(*) FILTER (WHERE {yr_count} >= 1) as patched," + f" COUNT(*) FILTER (WHERE {yr_count} = 1) as once," + f" COUNT(*) FILTER (WHERE {yr_count} >= 2) as twice," + f" COUNT(*) FILTER (WHERE {yr_count} >= 3) as thrice," + f" COUNT(*) FILTER (WHERE {yr_count} = 0 OR {yr_count} IS NULL) as never" + f" FROM server_audit_full WHERE status = 'ok'" + f" AND id IN (SELECT DISTINCT ON (hostname) id FROM server_audit_full WHERE status = 'ok' ORDER BY hostname, audit_date DESC)" + )).fetchone() + + patch_by_domain = db.execute(text( + f"SELECT d.name as domain, d.code," + f" COUNT(DISTINCT saf.hostname) as total," + f" COUNT(DISTINCT saf.hostname) FILTER (WHERE saf.{yr_count} >= 1) as patched," + f" COUNT(DISTINCT saf.hostname) FILTER (WHERE saf.{yr_count} >= 2) as twice," + f" COUNT(DISTINCT saf.hostname) FILTER (WHERE saf.{yr_count} = 0 OR saf.{yr_count} IS NULL) as never" + f" FROM server_audit_full saf JOIN servers s ON saf.server_id = s.id" + f" JOIN domain_environments de ON s.domain_env_id = de.id JOIN domains d ON de.domain_id = d.id" + f" WHERE saf.status = 'ok'" + f" AND saf.id IN (SELECT DISTINCT ON (hostname) id FROM server_audit_full WHERE status = 'ok' ORDER BY hostname, audit_date DESC)" + f" GROUP BY d.name, d.code, d.display_order ORDER BY d.display_order" + )).fetchall() + + patch_weekly = [] + if year == 2026: + patch_weekly = db.execute(text( + "SELECT last_patch_week as week, COUNT(*) as cnt FROM server_audit_full" + " WHERE status = 'ok' AND last_patch_year = 2026 AND last_patch_week IS NOT NULL" + " AND id IN (SELECT DISTINCT ON (hostname) id FROM server_audit_full WHERE status = 'ok' ORDER BY hostname, audit_date DESC)" + " GROUP BY last_patch_week ORDER BY last_patch_week" + )).fetchall() + + all_domains = db.execute(text("SELECT code, name, 'domain' as type FROM domains ORDER BY name")).fetchall() + all_zones = db.execute(text("SELECT name as code, name, 'zone' as type FROM zones ORDER BY name")).fetchall() + + servers = db.execute(text( + f"SELECT DISTINCT ON (saf.hostname) saf.id, saf.hostname, saf.os_release," + f" saf.last_patch_date, saf.last_patch_week, saf.last_patch_year," + f" saf.{yr_count} as patch_count, saf.{yr_weeks} as patch_weeks," + f" d.name as domain, e.name as env, z.name as zone" + f" FROM server_audit_full saf" + f" LEFT JOIN servers s ON saf.server_id = s.id" + f" LEFT JOIN domain_environments de ON s.domain_env_id = de.id" + f" LEFT JOIN domains d ON de.domain_id = d.id" + f" LEFT JOIN environments e ON de.environment_id = e.id" + f" LEFT JOIN zones z ON s.zone_id = z.id" + f" WHERE saf.status = 'ok'" + f" ORDER BY saf.hostname, saf.audit_date DESC" + )).fetchall() + + if domain: + zone_hosts = {r.hostname for r in db.execute(text( + "SELECT s.hostname FROM servers s JOIN zones z ON s.zone_id = z.id WHERE z.name = :n" + ), {"n": domain}).fetchall()} + if zone_hosts: + servers = [s for s in servers if s.hostname in zone_hosts] + else: + dom_hosts = {r.hostname for r in db.execute(text( + "SELECT s.hostname FROM servers s JOIN domain_environments de ON s.domain_env_id = de.id" + " JOIN domains d ON de.domain_id = d.id WHERE d.code = :dc" + ), {"dc": domain}).fetchall()} + servers = [s for s in servers if s.hostname in dom_hosts] + if search: + servers = [s for s in servers if search.lower() in s.hostname.lower()] + + if sort == "hostname": + servers.sort(key=lambda s: s.hostname.lower(), reverse=(sort_dir == "desc")) + elif sort == "count": + servers.sort(key=lambda s: s.patch_count or 0, reverse=(sort_dir == "desc")) + elif sort == "last": + servers.sort(key=lambda s: s.last_patch_week or "", reverse=(sort_dir == "desc")) + + total_filtered = len(servers) + total_pages = max(1, (total_filtered + per_page - 1) // per_page) + page = max(1, min(page, total_pages)) + servers_page = servers[(page - 1) * per_page : page * per_page] + + ctx = base_context(request, db, user) + ctx.update({ + "app_name": APP_NAME, "year": year, "kpis": kpis, + "patch_by_domain": patch_by_domain, "patch_weekly": patch_weekly, + "servers": servers_page, "all_domains": all_domains, "all_zones": all_zones, + "search": search, "domain": domain, "sort": sort, "sort_dir": sort_dir, + "page": page, "total_pages": total_pages, "total_filtered": total_filtered, + }) + return templates.TemplateResponse("audit_full_patching.html", ctx) + + @router.get("/audit-full/export-csv") async def audit_full_export_csv(request: Request, db=Depends(get_db)): user = get_current_user(request) diff --git a/app/templates/audit_full_patching.html b/app/templates/audit_full_patching.html new file mode 100644 index 0000000..c4c1e3f --- /dev/null +++ b/app/templates/audit_full_patching.html @@ -0,0 +1,134 @@ +{% extends 'base.html' %} +{% block title %}Patching {{ year }}{% endblock %} +{% block content %} +
| Domaine | Total | OK | 2x | Jamais | % |
|---|---|---|---|---|---|
| {{ d.domain }} | +{{ d.total }} | +{{ d.patched }} | +{{ d.twice }} | +{{ d.never }} | +
+
+
+
+ {{ dp }}%
+ |
+
| Hostname {% if sort == 'hostname' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% endif %} | +Domaine | +Env | +Zone | +Nb patches {% if sort == 'count' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% endif %} | +Semaines | +Dernier {% if sort == 'last' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% endif %} | +
|---|---|---|---|---|---|---|
| {{ s.hostname }} | +{{ s.domain or '-' }} | +{{ (s.env or '-')[:6] }} | +{% if s.zone == 'DMZ' %}DMZ{% else %}{{ s.zone or '-' }}{% endif %} | +{{ s.patch_count or 0 }} | +{% if s.patch_weeks %}{% for w in s.patch_weeks.split(',') %}{{ w }}{% endfor %}{% else %}-{% endif %} | +{% if s.last_patch_date %}{{ s.last_patch_date }}{% elif s.last_patch_week %}{{ s.last_patch_week }}{% else %}-{% endif %} | +