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
+
+
+ | Package | Version | Findings |
+
+
+ {% for p in most_flagged %}
+
+ | {{ p.package_name }} |
+ {{ p.package_version }} |
+
+ {{ p.total }}
+
+ |
+
+ {% endfor %}
+
+
+
+{% endif %}
+
+{% if latest_flagged %}
+
+
🔴 Latest Flagged
+
+
+ | Package | Version | Findings | Time |
+
+
+ {% for s in latest_flagged %}
+
+ | {{ s.package_name }} |
+ {{ s.package_version }} |
+ {{ s.total_findings }} |
+ {{ s.started_at.strftime('%m-%d %H:%M') if s.started_at }} |
+
+ {% endfor %}
+
+
+
+{% endif %}
+
+
Latest Scans
| Package |
Version |
- Ecosystem |
+ Repo |
Status |
- Findings |
+ |
Time |
@@ -34,17 +117,18 @@
| {{ 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 }} |
{% endfor %}
+
View all scans →
{% if top_rules %}
-
Top Rules Triggered
+
Top Rules Triggered
| Rule | Count |
@@ -54,3 +138,5 @@
{% endif %}
+
+
Last refresh: {{ now.strftime('%H:%M:%S') }} (auto every 30s)