from fastapi import APIRouter, Request, Depends 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("/dashboard", response_class=HTMLResponse) async def dashboard(request: Request, db=Depends(get_db)): user = get_current_user(request) if not user: return RedirectResponse(url="/login") # Stats generales stats = {} stats["total_servers"] = db.execute(text("SELECT COUNT(*) FROM servers")).scalar() stats["patchable"] = db.execute(text("SELECT COUNT(*) FROM servers WHERE patch_os_owner='secops' AND etat='en_production'")).scalar() stats["linux"] = db.execute(text("SELECT COUNT(*) FROM servers WHERE os_family='linux'")).scalar() stats["windows"] = db.execute(text("SELECT COUNT(*) FROM servers WHERE os_family='windows'")).scalar() stats["decom"] = db.execute(text("SELECT COUNT(*) FROM servers WHERE etat='decommissionne'")).scalar() stats["eol"] = db.execute(text("SELECT COUNT(*) FROM servers WHERE licence_support='eol'")).scalar() stats["qualys_assets"] = db.execute(text("SELECT COUNT(*) FROM qualys_assets")).scalar() stats["qualys_tags"] = db.execute(text("SELECT COUNT(*) FROM qualys_tags")).scalar() # Par domaine domains = db.execute(text(""" SELECT d.name, d.code, COUNT(s.id) as total, COUNT(*) FILTER (WHERE s.etat='en_production') as actifs, COUNT(*) FILTER (WHERE s.os_family='linux') as linux, COUNT(*) FILTER (WHERE s.os_family='windows') as windows FROM servers s JOIN domain_environments de ON s.domain_env_id = de.id JOIN domains d ON de.domain_id = d.id GROUP BY d.name, d.code, d.display_order ORDER BY d.display_order """)).fetchall() # Par tier tiers = db.execute(text("SELECT tier, COUNT(*) FROM servers GROUP BY tier ORDER BY tier")).fetchall() # ── Stats patching 2026 ── patch_stats = db.execute(text(""" SELECT COUNT(*) as audited, COUNT(*) FILTER (WHERE last_patch_year = 2026) as patched_2026, COUNT(*) FILTER (WHERE last_patch_year = 2025) as patched_2025_only, COUNT(*) FILTER (WHERE last_patch_year IS NULL OR last_patch_week IS NULL) as never_patched, COUNT(*) FILTER (WHERE patch_count_2026 >= 1) as patched_once, COUNT(*) FILTER (WHERE patch_count_2026 >= 2) as patched_twice, COUNT(*) FILTER (WHERE patch_count_2026 >= 3) as patched_thrice, COUNT(*) FILTER (WHERE reboot_required = true) as needs_reboot FROM server_audit_full WHERE status = 'ok' AND id IN (SELECT DISTINCT ON (hostname) id FROM server_audit_full WHERE status = 'ok' ORDER BY hostname, audit_date DESC) """)).fetchone() # Frequence patching par semaine 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() # Patching par domaine patch_by_domain = db.execute(text(""" SELECT d.name as domain, d.code, COUNT(DISTINCT saf.hostname) as total, COUNT(DISTINCT saf.hostname) FILTER (WHERE saf.last_patch_year = 2026) as patched, COUNT(DISTINCT saf.hostname) FILTER (WHERE saf.patch_count_2026 >= 2) as patched_twice, COUNT(DISTINCT saf.hostname) FILTER (WHERE saf.last_patch_year IS NULL) as never FROM server_audit_full saf JOIN servers s ON saf.server_id = s.id JOIN domain_environments de ON s.domain_env_id = de.id JOIN domains d ON de.domain_id = d.id WHERE saf.status = 'ok' AND saf.id IN (SELECT DISTINCT ON (hostname) id FROM server_audit_full WHERE status = 'ok' ORDER BY hostname, audit_date DESC) GROUP BY d.name, d.code, d.display_order ORDER BY d.display_order """)).fetchall() # Patching par environnement patch_by_env = db.execute(text(""" SELECT e.name as env, COUNT(DISTINCT saf.hostname) as total, COUNT(DISTINCT saf.hostname) FILTER (WHERE saf.last_patch_year = 2026) as patched FROM server_audit_full saf JOIN servers s ON saf.server_id = s.id JOIN domain_environments de ON s.domain_env_id = de.id JOIN environments e ON de.environment_id = e.id WHERE saf.status = 'ok' AND saf.id IN (SELECT DISTINCT ON (hostname) id FROM server_audit_full WHERE status = 'ok' ORDER BY hostname, audit_date DESC) GROUP BY e.name ORDER BY e.name """)).fetchall() # Patching par zone (DMZ, LAN, EMV) patch_by_zone = db.execute(text(""" SELECT z.name as zone, COUNT(DISTINCT saf.hostname) as total, COUNT(DISTINCT saf.hostname) FILTER (WHERE saf.last_patch_year = 2026) as patched, COUNT(DISTINCT saf.hostname) FILTER (WHERE saf.last_patch_year IS NULL) as never FROM server_audit_full saf JOIN servers s ON saf.server_id = s.id JOIN zones z ON s.zone_id = z.id WHERE saf.status = 'ok' AND saf.id IN (SELECT DISTINCT ON (hostname) id FROM server_audit_full WHERE status = 'ok' ORDER BY hostname, audit_date DESC) GROUP BY z.name ORDER BY z.name """)).fetchall() return templates.TemplateResponse("dashboard.html", { "request": request, "user": user, "app_name": APP_NAME, "stats": stats, "domains": domains, "tiers": tiers, "patch_stats": patch_stats, "patch_weekly": patch_weekly, "patch_by_domain": patch_by_domain, "patch_by_env": patch_by_env, "patch_by_zone": patch_by_zone, })