fix: критические баги и качество кода — полный аудит

Критические фиксы:
- main.py: монтировать /static из web/static/ (CSS не грузился совсем)
- api/scans.py: filtered total count (был всегда общий, игнорируя фильтры)
- web/routes.py: исправлен VALID_SORT_FIELDS (отсутствовали ключи packages)
- web/routes.py: filtered total count для web scans list
- package_detail.html: f.data.X вместо f.X (findings не отображались)

Чистка мёртвого кода:
- config.py: удалён _parse_repos и nexus_repositories (не использовались)
- web/routes.py: удалён completed_scans/failed_scans (не отображались)
- удалён мёртвый guarddog_nexus/static/style.css (67-байтный стаб)

Качество кода:
- web/routes.py: Jinja2 Environment кэшируется на уровне модуля
- Вынесен дублирующийся JS в web/static/app.js
- Вынесены дублирующиеся inline-стили в CSS-классы
- Исправлен duplicate class attribute в списках
- Удалены гигантские SVG из empty states

Тесты:
- 20 новых edge-case тестов (CSV export, search/filter/sort, 404, pagination)
- Добавлен sample_flagged_scan fixture
- Итого: 50 тестов, все зелёные
This commit is contained in:
Marker689
2026-05-10 03:46:05 +03:00
parent 6c8e89c95e
commit c43e7c4c9b
15 changed files with 329 additions and 145 deletions

View File

@@ -4,6 +4,7 @@ import datetime
from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse
from jinja2 import Environment, PackageLoader, select_autoescape
from sqlalchemy import Integer, cast, func, select, text
from sqlalchemy.ext.asyncio import AsyncSession
@@ -12,7 +13,12 @@ from guarddog_nexus.models import Finding, Scan
router = APIRouter(tags=["web"])
VALID_SORT_FIELDS = {
_jinja_env = Environment(
loader=PackageLoader("guarddog_nexus", "web/templates"),
autoescape=select_autoescape(),
)
SCAN_SORT_FIELDS = {
"id": Scan.id,
"package_name": Scan.package_name,
"started_at": Scan.started_at,
@@ -20,15 +26,16 @@ VALID_SORT_FIELDS = {
"total_findings": Scan.total_findings,
}
PACKAGE_SORT_FIELDS = {
"name": Scan.package_name,
"last_scanned_at": Scan.started_at,
"total_findings": Scan.total_findings,
"flagged": Scan.flagged,
}
def _render(name: str, **context) -> HTMLResponse:
from jinja2 import Environment, PackageLoader, select_autoescape
env = Environment(
loader=PackageLoader("guarddog_nexus", "web/templates"),
autoescape=select_autoescape(),
)
template = env.get_template(name)
template = _jinja_env.get_template(name)
return HTMLResponse(template.render(**context))
@@ -53,10 +60,6 @@ async def _dashboard_data(session: AsyncSession) -> dict:
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(
@@ -115,7 +118,6 @@ async def _dashboard_data(session: AsyncSession) -> dict:
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(
@@ -133,8 +135,6 @@ async def _dashboard_data(session: AsyncSession) -> dict:
"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,
@@ -162,23 +162,27 @@ async def scans_list(
per_page = 50
offset = (page - 1) * per_page
count_q = select(func.count(Scan.id))
q = select(Scan)
if flagged == "1":
q = q.where(Scan.flagged == True)
count_q = count_q.where(Scan.flagged == True)
if status:
q = q.where(Scan.status == status)
count_q = count_q.where(Scan.status == status)
if search:
pattern = f"%{search}%"
q = q.where(
Scan.package_name.ilike(pattern) | Scan.package_version.ilike(pattern)
)
condition = Scan.package_name.ilike(pattern) | Scan.package_version.ilike(pattern)
q = q.where(condition)
count_q = count_q.where(condition)
sort_field = VALID_SORT_FIELDS.get(sort_by, Scan.started_at)
sort_field = SCAN_SORT_FIELDS.get(sort_by, Scan.started_at)
q = q.order_by(sort_field.desc() if sort_dir == "desc" else sort_field.asc())
q = q.offset(offset).limit(per_page)
scans = (await session.execute(q)).scalars().all()
total = await session.scalar(select(func.count(Scan.id)))
total = await session.scalar(count_q)
return _render(
"scans_list.html",
@@ -240,17 +244,17 @@ async def packages_list(
Scan.package_name.ilike(pattern) | Scan.package_version.ilike(pattern)
)
sort_field = VALID_SORT_FIELDS.get(sort_by, Scan.started_at)
sort_field = PACKAGE_SORT_FIELDS.get(sort_by, Scan.started_at)
sort_col = func.max(sort_field)
subq = subq.order_by(
sort_col.desc() if sort_dir == "desc" else sort_col.asc()
)
subq = subq.subquery()
total = await session.scalar(select(func.count()).select_from(subq))
sq = subq.subquery()
total = await session.scalar(select(func.count()).select_from(sq))
rows = (
await session.execute(
select(subq).offset(offset).limit(per_page)
select(sq).offset(offset).limit(per_page)
)
).all()