Aligne la colonne servers.etat sur les valeurs iTop exactes au lieu des codes lowercase internes. Impact: - servers.etat stocke: Production, Implémentation, Stock, Obsolète, EOL, prêt, tests, Nouveau, A récupérer, Cassé, Cédé, En panne, Perdu, Recyclé, Occasion, A détruire, Volé - Remplace tous les 'production'/'obsolete'/'stock'/'eol'/'implementation' en WHERE/comparisons par les labels iTop verbatim (~10 fichiers) - Templates badges/filtres: valeurs + labels iTop - itop_service: maintient mapping iTop API internal code <-> DB label - import_sanef_*: norm_etat retourne la valeur iTop verbatim ou None (plus de fallback silencieux sur 'production') Ajoute: - tools/import_etat_itop.py : migration lowercase -> iTop + re-import CSV - tools/import_environnement.py : fix dry-run pour ADD COLUMN idempotent Supprime: - tools/fix_etat_extend.py (obsolete par import_etat_itop.py)
125 lines
6.6 KiB
Python
125 lines
6.6 KiB
Python
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='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='Obsolète'")).scalar()
|
|
stats["obsolete"] = db.execute(text("SELECT COUNT(*) FROM servers WHERE licence_support='obsolete'")).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()
|
|
stats["qualys_active"] = db.execute(text("SELECT COUNT(*) FROM qualys_assets WHERE agent_status ILIKE '%active%' AND agent_status NOT ILIKE '%inactive%'")).scalar()
|
|
stats["qualys_inactive"] = db.execute(text("SELECT COUNT(*) FROM qualys_assets WHERE agent_status ILIKE '%inactive%'")).scalar()
|
|
stats["qualys_no_agent"] = db.execute(text("SELECT COUNT(*) FROM servers WHERE etat='Production' AND NOT EXISTS (SELECT 1 FROM qualys_assets qa WHERE LOWER(qa.hostname) = LOWER(servers.hostname))")).scalar()
|
|
|
|
# Par domaine
|
|
domains = db.execute(text("""
|
|
SELECT d.name, d.code, COUNT(s.id) as total,
|
|
COUNT(*) FILTER (WHERE s.etat='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 IN ('ok','partial')
|
|
AND id IN (SELECT DISTINCT ON (hostname) id FROM server_audit_full WHERE status IN ('ok','partial') 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 IN ('ok','partial') 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 IN ('ok','partial') 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 IN ('ok','partial')
|
|
AND saf.id IN (SELECT DISTINCT ON (hostname) id FROM server_audit_full WHERE status IN ('ok','partial') 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 IN ('ok','partial')
|
|
AND saf.id IN (SELECT DISTINCT ON (hostname) id FROM server_audit_full WHERE status IN ('ok','partial') 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 IN ('ok','partial')
|
|
AND saf.id IN (SELECT DISTINCT ON (hostname) id FROM server_audit_full WHERE status IN ('ok','partial') 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,
|
|
})
|