Servers: filtre licence (active/obsolete/els/sans licence)

This commit is contained in:
Pierre & Lumière 2026-04-14 22:17:09 +02:00
parent 2a11a27675
commit e2b984c2c4
4 changed files with 109 additions and 2 deletions

View File

@ -0,0 +1,96 @@
"""Router Historique patching — vue de patch_history"""
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("/patching/historique", response_class=HTMLResponse)
async def patch_history_page(request: Request, db=Depends(get_db),
year: int = Query(None), week: int = Query(None),
hostname: str = Query(None), page: int = Query(1)):
user = get_current_user(request)
if not user:
return RedirectResponse(url="/login")
from datetime import datetime
if not year:
year = datetime.now().year
per_page = 100
offset = (page - 1) * per_page
# KPIs
kpis = {}
kpis["total"] = db.execute(text(
"SELECT COUNT(*) FROM patch_history WHERE EXTRACT(YEAR FROM date_patch)=:y"
), {"y": year}).scalar()
kpis["servers"] = db.execute(text(
"SELECT COUNT(DISTINCT server_id) FROM patch_history WHERE EXTRACT(YEAR FROM date_patch)=:y"
), {"y": year}).scalar()
kpis["patchables"] = db.execute(text(
"SELECT COUNT(*) FROM servers WHERE etat='Production' AND patch_os_owner='secops'"
)).scalar()
kpis["never"] = db.execute(text("""
SELECT COUNT(*) FROM servers s
WHERE s.etat='Production' AND s.patch_os_owner='secops'
AND NOT EXISTS (SELECT 1 FROM patch_history ph
WHERE ph.server_id=s.id AND EXTRACT(YEAR FROM ph.date_patch)=:y)
"""), {"y": year}).scalar()
kpis["coverage_pct"] = round((kpis["servers"] / kpis["patchables"] * 100), 1) if kpis["patchables"] else 0
# Par semaine
by_week = db.execute(text("""
SELECT TO_CHAR(date_patch, 'IW') as week_num,
COUNT(DISTINCT server_id) as servers
FROM patch_history
WHERE EXTRACT(YEAR FROM date_patch)=:y
GROUP BY TO_CHAR(date_patch, 'IW')
ORDER BY week_num
"""), {"y": year}).fetchall()
# Filtres
where = ["EXTRACT(YEAR FROM ph.date_patch)=:y"]
params = {"y": year, "limit": per_page, "offset": offset}
if week:
where.append("EXTRACT(WEEK FROM ph.date_patch)=:wk")
params["wk"] = week
if hostname:
where.append("s.hostname ILIKE :h")
params["h"] = f"%{hostname}%"
wc = " AND ".join(where)
total_filtered = db.execute(text(
f"SELECT COUNT(*) FROM patch_history ph JOIN servers s ON ph.server_id=s.id WHERE {wc}"
), params).scalar()
rows = db.execute(text(f"""
SELECT s.id as sid, s.hostname, s.os_family, s.etat,
ph.date_patch, ph.status, ph.notes,
z.name as zone
FROM patch_history ph
JOIN servers s ON ph.server_id = s.id
LEFT JOIN zones z ON s.zone_id = z.id
WHERE {wc}
ORDER BY ph.date_patch DESC
LIMIT :limit OFFSET :offset
"""), params).fetchall()
# Années dispo
years = db.execute(text("""
SELECT DISTINCT EXTRACT(YEAR FROM date_patch)::int as y
FROM patch_history ORDER BY y DESC
""")).fetchall()
return templates.TemplateResponse("patch_history.html", {
"request": request, "user": user, "app_name": APP_NAME,
"kpis": kpis, "by_week": by_week, "rows": rows,
"year": year, "week": week, "hostname": hostname,
"page": page, "per_page": per_page, "total_filtered": total_filtered,
"years": [y.y for y in years],
})

View File

@ -20,7 +20,7 @@ async def servers_list(request: Request, db=Depends(get_db),
domain: str = Query(None), env: str = Query(None),
tier: str = Query(None), etat: str = Query(None),
os: str = Query(None), owner: str = Query(None),
zone: str = Query(None),
zone: str = Query(None), licence: str = Query(None),
application: str = Query(None), application_id: int = Query(None),
search: str = Query(None), page: int = Query(1),
sort: str = Query("hostname"), sort_dir: str = Query("asc")):
@ -29,7 +29,7 @@ async def servers_list(request: Request, db=Depends(get_db),
return RedirectResponse(url="/login")
filters = {"domain": domain, "env": env, "tier": tier, "etat": etat, "os": os,
"owner": owner, "zone": zone,
"owner": owner, "zone": zone, "licence": licence,
"application": application, "application_id": application_id,
"search": search}
servers, total = list_servers(db, filters, page, sort=sort, sort_dir=sort_dir)

View File

@ -132,6 +132,11 @@ def list_servers(db, filters, page=1, per_page=50, sort="hostname", sort_dir="as
params["zone"] = filters["zone"]
if filters.get("owner"):
where.append("s.patch_os_owner = :owner"); params["owner"] = filters["owner"]
if filters.get("licence"):
if filters["licence"] == "__null__":
where.append("s.licence_support IS NULL")
else:
where.append("s.licence_support = :licence"); params["licence"] = filters["licence"]
if filters.get("application_id"):
where.append("s.application_id = :app_id"); params["app_id"] = filters["application_id"]
elif filters.get("application"):

View File

@ -53,6 +53,12 @@
<option value="ipop" {% if filters.owner == 'ipop' %}selected{% endif %}>ipop</option>
<option value="na" {% if filters.owner == 'na' %}selected{% endif %}>na</option>
</select>
<select name="licence" onchange="this.form.submit()"><option value="">Licence</option>
<option value="active" {% if filters.licence == 'active' %}selected{% endif %}>active</option>
<option value="obsolete" {% if filters.licence == 'obsolete' %}selected{% endif %}>obsolete (EOL)</option>
<option value="els" {% if filters.licence == 'els' %}selected{% endif %}>els</option>
<option value="__null__" {% if filters.licence == '__null__' %}selected{% endif %}>(Sans licence)</option>
</select>
<select name="application" onchange="this.form.submit()" style="max-width:200px"><option value="">Solution app.</option>
{% for a in applications_list %}<option value="{{ a.application_name }}" {% if filters.application == a.application_name %}selected{% endif %}>{{ a.application_name }} ({{ a.c }})</option>{% endfor %}
</select>