feat: LLM-анализ — индикатор прогресса, кнопка рескана, статистика на дашборде

- Добавлен статус {"status": "analyzing"} в finding.report на время LLM-анализа
- Кнопка рескана (Retry) под LLM-отчётом в ручном режиме
- LLM-статистика на дашборде: analysed / pending
- Защита от двойного анализа через per-finding asyncio.Lock
- _llm_spinner.html — фрагмент спиннера для состояния analysing
- Удалён мёртвый код: constants, i18n, CSS, queries
- Фиксы: _env_int, индексы БД, UnicodeDecodeError, time.mktime и др.
- Шаблоны: shared includes (_status_badge, _pagination)
- AGENTS.md: workflow (lint, test, commit, rebuild)
This commit is contained in:
Marker689
2026-05-10 09:54:04 +03:00
parent c99a7bf67c
commit 6984844161
26 changed files with 261 additions and 266 deletions

View File

@@ -43,6 +43,9 @@ async def scan_package(filepath: str, ecosystem: str = DEFAULT_ECOSYSTEM) -> dic
log.error("GuardDog exited %d: %s", proc.returncode, stderr.decode())
return {"findings": [], "errors": [stderr.decode().strip()]}
if proc.returncode == 1 and stderr:
log.warning("GuardDog stderr (exit 1): %s", stderr.decode().strip())
try:
data = json.loads(stdout.decode())
except json.JSONDecodeError:
@@ -96,6 +99,17 @@ def _normalize_output(data: dict) -> dict:
)
elif isinstance(value, dict) and not value:
continue
elif isinstance(value, dict):
# Non-empty dict — treat as a single finding
findings.append(
{
"rule": rule_name,
"severity": value.get("severity", DEFAULT_FINDING_SEVERITY),
"message": value.get("message", ""),
"location": value.get("location", ""),
"code": value.get("code", ""),
}
)
errors = data.get("errors", {})
if isinstance(errors, dict):