From e83167a938b80bfa90e3229b886beab369625c55 Mon Sep 17 00:00:00 2001 From: Marker689 Date: Sat, 9 May 2026 05:35:36 +0300 Subject: [PATCH] feat: rich dashboard with severity bars, heatmap, most flagged, live poll --- guarddog_nexus/web/routes.py | 94 +++++++++++++-- guarddog_nexus/web/templates/dashboard.html | 2 +- .../web/templates/dashboard_stats.html | 110 ++++++++++++++++-- 3 files changed, 181 insertions(+), 25 deletions(-) diff --git a/guarddog_nexus/web/routes.py b/guarddog_nexus/web/routes.py index f03aced..e55f642 100644 --- a/guarddog_nexus/web/routes.py +++ b/guarddog_nexus/web/routes.py @@ -4,7 +4,7 @@ import datetime from fastapi import APIRouter, Depends, Request from fastapi.responses import HTMLResponse -from sqlalchemy import func, select +from sqlalchemy import Integer, cast, func, select from sqlalchemy.ext.asyncio import AsyncSession from guarddog_nexus.database import get_session @@ -27,6 +27,17 @@ def _render(name: str, **context) -> HTMLResponse: @router.get("/", response_class=HTMLResponse) async def dashboard(request: Request, session: AsyncSession = Depends(get_session)): + ctx = await _dashboard_data(session) + return _render("dashboard.html", **ctx, request=request) + + +@router.get("/dashboard/stats", response_class=HTMLResponse) +async def dashboard_stats_fragment(session: AsyncSession = Depends(get_session)): + ctx = await _dashboard_data(session) + return _render("dashboard_stats.html", **ctx) + + +async def _dashboard_data(session: AsyncSession) -> dict: total_scans = await session.scalar(select(func.count(Scan.id))) flagged_scans = await session.scalar(select(func.count(Scan.id)).where(Scan.flagged == True)) recent_flagged = await session.scalar( @@ -35,7 +46,29 @@ async def dashboard(request: Request, session: AsyncSession = Depends(get_sessio Scan.started_at >= func.datetime("now", "-7 days"), ) ) + completed_scans = await session.scalar( + select(func.count(Scan.id)).where(Scan.status == "completed") + ) + failed_scans = await session.scalar(select(func.count(Scan.id)).where(Scan.status == "failed")) total_findings = await session.scalar(select(func.count(Finding.id))) + + warnings_count = await session.scalar( + select(func.count(Finding.id)).where(Finding.severity == "WARNING") + ) + errors_count = await session.scalar( + select(func.count(Finding.id)).where(Finding.severity == "ERROR") + ) + + latest_flagged = ( + ( + await session.execute( + select(Scan).where(Scan.flagged == True).order_by(Scan.started_at.desc()).limit(8) + ) + ) + .scalars() + .all() + ) + latest_scans = ( (await session.execute(select(Scan).order_by(Scan.started_at.desc()).limit(10))) .scalars() @@ -51,17 +84,54 @@ async def dashboard(request: Request, session: AsyncSession = Depends(get_sessio ) ).all() - return _render( - "dashboard.html", - total_scans=total_scans, - flagged_scans=flagged_scans, - recent_flagged=recent_flagged, - total_findings=total_findings, - latest_scans=latest_scans, - top_rules=[(r.rule, r.cnt) for r in top_rules], - now=datetime.datetime.now(datetime.timezone.utc), - request=request, - ) + most_flagged = ( + await session.execute( + select( + Scan.package_name, + Scan.package_version, + func.sum(Scan.total_findings).label("total"), + func.max(Scan.started_at).label("last_scan"), + ) + .where(Scan.flagged == True) + .group_by(Scan.package_name, Scan.package_version) + .order_by(func.sum(Scan.total_findings).desc()) + .limit(8) + ) + ).all() + + max_findings = max((r.total for r in most_flagged), default=1) + + # Heatmap: scans per day for last 14 days + days_raw = ( + await session.execute( + select( + func.date(Scan.started_at).label("day"), + func.count(Scan.id).label("cnt"), + func.sum(cast(Scan.flagged, Integer)).label("flagged_cnt"), + ) + .where(Scan.started_at >= func.datetime("now", "-14 days")) + .group_by("day") + .order_by("day") + ) + ).all() + + return { + "total_scans": total_scans or 0, + "flagged_scans": flagged_scans or 0, + "recent_flagged": recent_flagged or 0, + "completed_scans": completed_scans or 0, + "failed_scans": failed_scans or 0, + "total_findings": total_findings or 0, + "warnings_count": warnings_count or 0, + "errors_count": errors_count or 0, + "latest_flagged": latest_flagged, + "latest_scans": latest_scans, + "top_rules": [(r.rule, r.cnt) for r in top_rules], + "most_flagged": most_flagged, + "max_findings": max_findings, + "days": [(d.day, d.cnt, d.flagged_cnt) for d in days_raw], + "now": datetime.datetime.now(datetime.timezone.utc), + } @router.get("/scans", response_class=HTMLResponse) diff --git a/guarddog_nexus/web/templates/dashboard.html b/guarddog_nexus/web/templates/dashboard.html index cf16025..d55d475 100644 --- a/guarddog_nexus/web/templates/dashboard.html +++ b/guarddog_nexus/web/templates/dashboard.html @@ -2,7 +2,7 @@ {% block content %}

Dashboard

-
+
{% include "dashboard_stats.html" %}
{% endblock %} diff --git a/guarddog_nexus/web/templates/dashboard_stats.html b/guarddog_nexus/web/templates/dashboard_stats.html index abf1b16..9aee305 100644 --- a/guarddog_nexus/web/templates/dashboard_stats.html +++ b/guarddog_nexus/web/templates/dashboard_stats.html @@ -1,31 +1,114 @@
-
{{ total_scans }}
+

{{ total_scans }}

Total Scans
-
{{ flagged_scans }}
- Flagged +

{{ flagged_scans }}

+ ⚠ Flagged
-
{{ recent_flagged }}
+

{{ recent_flagged }}

Flagged (7 days)
-
{{ total_findings }}
+

{{ total_findings }}

Total Findings
+
+

{{ errors_count }}

+ Errors +
+
+

{{ warnings_count }}

+ Warnings +
-

Latest Scans

+{% if total_findings > 0 %} +
+ Severity ratio +
+ {% set err_pct = (errors_count / total_findings * 100) | int %} + {% set warn_pct = 100 - err_pct %} +
+
+
+
+ ERROR {{ errors_count }} + WARNING {{ warnings_count }} +
+
+{% endif %} + +{% if days %} +
+ Scan activity (14 days) +
+ {% set max_cnt = days | map(attribute=1) | max %} + {% for day, cnt, fl in days %} +
+ {% set h = (cnt / max_cnt * 38) | int if max_cnt > 0 else 0 %} +
+
+ {% endfor %} +
+
+{% endif %} + +{% if most_flagged %} +
+

⚠ Most Flagged Packages

+ + + + + + {% for p in most_flagged %} + + + + + + {% endfor %} + +
PackageVersionFindings
{{ p.package_name }}{{ p.package_version }} + {{ p.total }} + +
+
+{% endif %} + +{% if latest_flagged %} +
+

🔴 Latest Flagged

+ + + + + + {% for s in latest_flagged %} + + + + + + + {% endfor %} + +
PackageVersionFindingsTime
{{ s.package_name }}{{ s.package_version }}{{ s.total_findings }}{{ s.started_at.strftime('%m-%d %H:%M') if s.started_at }}
+
+{% endif %} + +

Latest Scans

- + - + @@ -34,17 +117,18 @@ - + - - + + {% endfor %}
Package VersionEcosystemRepo StatusFindings Time
{{ s.package_name }} {{ s.package_version }}{{ s.ecosystem }}{{ s.repository }} {{ s.status }}{% if s.flagged %}{{ s.total_findings }}{% else %}{{ s.total_findings }}{% endif %}{{ s.started_at.strftime('%Y-%m-%d %H:%M') if s.started_at }}{% if s.flagged %}⚠ {{ s.total_findings }}{% elif s.status == 'completed' %}{% else %}-{% endif %}{{ s.started_at.strftime('%m-%d %H:%M') if s.started_at }}
+View all scans → {% if top_rules %} -

Top Rules Triggered

+

Top Rules Triggered

@@ -54,3 +138,5 @@
RuleCount
{% endif %} + +Last refresh: {{ now.strftime('%H:%M:%S') }} (auto every 30s)