fix(i18n): формальный русский перевод + t() во всех шаблонах

- i18n.py: формальный русский (сканирования, панель управления, и т.д.)
- Все шаблоны: замена хардкод-строк на t(key, request.state.lang)
- dashboard_stats_fragment: добавлен request в контекст
This commit is contained in:
Marker689
2026-05-10 07:52:58 +03:00
parent f108464828
commit d11be24c5f
10 changed files with 129 additions and 126 deletions

View File

@@ -1,22 +1,25 @@
"""Internationalisation — RU / EN dictionaries and helpers.""" """Internationalisation — RU / EN dictionaries and helpers."""
_STRINGS = { _STRINGS = {
"nav_dashboard": {"en": "Dashboard", "ru": "Дашборд"}, "nav_dashboard": {"en": "Dashboard", "ru": "Панель"},
"nav_scans": {"en": "Scans", "ru": "Сканы"}, "nav_scans": {"en": "Scans", "ru": "Сканирования"},
"nav_packages": {"en": "Packages", "ru": "Пакеты"}, "nav_packages": {"en": "Packages", "ru": "Пакеты"},
"title_dashboard": {"en": "GuardDog Nexus", "ru": "GuardDog Nexus"}, "title_dashboard": {"en": "Dashboard — GuardDog Nexus", "ru": "Панель — GuardDog Nexus"},
"title_scans": {"en": "Scans — GuardDog Nexus", "ru": "Сканы — GuardDog Nexus"}, "title_scans": {"en": "Scans — GuardDog Nexus", "ru": "Сканирования — GuardDog Nexus"},
"title_packages": {"en": "Packages — GuardDog Nexus", "ru": "Пакеты — GuardDog Nexus"}, "title_packages": {"en": "Packages — GuardDog Nexus", "ru": "Пакеты — GuardDog Nexus"},
"title_scan_detail": {"en": "Scan #{} — GuardDog Nexus", "ru": "Скан #{} — GuardDog Nexus"}, "title_scan_detail": {
"en": "Scan #{} — GuardDog Nexus",
"ru": "Сканирование #{} — GuardDog Nexus",
},
"title_package_detail": {"en": "{} v{} — GuardDog Nexus", "ru": "{} v{} — GuardDog Nexus"}, "title_package_detail": {"en": "{} v{} — GuardDog Nexus", "ru": "{} v{} — GuardDog Nexus"},
"heading_scans": {"en": "Scans", "ru": "Сканы"}, "heading_dashboard": {"en": "Dashboard", "ru": "Панель управления"},
"heading_scans": {"en": "Scans", "ru": "Сканирования"},
"heading_packages": {"en": "Packages", "ru": "Пакеты"}, "heading_packages": {"en": "Packages", "ru": "Пакеты"},
"heading_dashboard": {"en": "Dashboard", "ru": "Дашборд"}, "heading_latest_flagged": {"en": "Latest Flagged", "ru": "Последние обнаружения"},
"heading_latest_flagged": {"en": "Latest Flagged", "ru": "Последние флаги"}, "heading_latest_scans": {"en": "Latest Scans", "ru": "Последние сканирования"},
"heading_latest_scans": {"en": "Latest Scans", "ru": "Последние сканы"},
"heading_findings": {"en": "Findings", "ru": "Находки"}, "heading_findings": {"en": "Findings", "ru": "Находки"},
"heading_findings_count": {"en": "Findings ({})", "ru": "Находки ({})"}, "heading_findings_count": {"en": "Findings ({})", "ru": "Находки ({})"},
"heading_scans_count": {"en": "Scans ({})", "ru": "Сканы ({})"}, "heading_scans_count": {"en": "Scans ({})", "ru": "Сканирований ({})"},
"col_id": {"en": "ID", "ru": "ID"}, "col_id": {"en": "ID", "ru": "ID"},
"col_package": {"en": "Package", "ru": "Пакет"}, "col_package": {"en": "Package", "ru": "Пакет"},
"col_version": {"en": "Version", "ru": "Версия"}, "col_version": {"en": "Version", "ru": "Версия"},
@@ -25,53 +28,53 @@ _STRINGS = {
"col_status": {"en": "Status", "ru": "Статус"}, "col_status": {"en": "Status", "ru": "Статус"},
"col_findings": {"en": "Findings", "ru": "Находки"}, "col_findings": {"en": "Findings", "ru": "Находки"},
"col_time": {"en": "Time", "ru": "Время"}, "col_time": {"en": "Time", "ru": "Время"},
"col_name": {"en": "Name", "ru": "Имя"}, "col_name": {"en": "Name", "ru": "Название"},
"col_ecosystem": {"en": "Ecosystem", "ru": "Экосистема"}, "col_ecosystem": {"en": "Ecosystem", "ru": "Экосистема"},
"col_flagged": {"en": "Flagged", "ru": "Флаг"}, "col_flagged": {"en": "Flagged", "ru": "Помечен"},
"col_last_scan": {"en": "Last Scan", "ru": "Посл. скан"}, "col_last_scan": {"en": "Last Scan", "ru": "Последнее сканирование"},
"filter_search": {"en": "Search packages...", "ru": "Поиск пакетов..."}, "filter_search": {"en": "Search packages...", "ru": "Поиск пакетов..."},
"filter_all_statuses": {"en": "All Statuses", "ru": "Все статусы"}, "filter_all_statuses": {"en": "All Statuses", "ru": "Все статусы"},
"filter_pending": {"en": "Pending", "ru": "Ожидает"}, "filter_pending": {"en": "Pending", "ru": "Ожидание"},
"filter_scanning": {"en": "Scanning", "ru": "Сканируется"}, "filter_scanning": {"en": "Scanning", "ru": "Сканирование"},
"filter_completed": {"en": "Completed", "ru": "Завершён"}, "filter_completed": {"en": "Completed", "ru": "Завершено"},
"filter_failed": {"en": "Failed", "ru": "Ошибка"}, "filter_failed": {"en": "Failed", "ru": "Ошибка"},
"btn_show_all": {"en": "Show all", "ru": "Показать все"}, "btn_show_all": {"en": "Show all", "ru": "Показать все"},
"btn_flagged_only": {"en": "Flagged only", "ru": "Только флаги"}, "btn_flagged_only": {"en": "Flagged only", "ru": "Только помеченные"},
"btn_export_csv": {"en": "Export CSV", "ru": "Экспорт CSV"}, "btn_export_csv": {"en": "Export CSV", "ru": "Экспорт CSV"},
"btn_analyze_llm": {"en": "Analyze with LLM", "ru": "Анализ через LLM"}, "btn_analyze_llm": {"en": "Analyze with LLM", "ru": "Анализировать через LLM"},
"btn_copy": {"en": "Copy", "ru": "Копировать"}, "btn_copy": {"en": "Copy", "ru": "Копировать"},
"btn_prev": {"en": "Prev", "ru": "Назад"}, "btn_prev": {"en": "Prev", "ru": "Пред."},
"btn_next": {"en": "Next", "ru": "Вперёд"}, "btn_next": {"en": "Next", "ru": "След."},
"scan_info_package": {"en": "Package", "ru": "Пакет"}, "scan_info_package": {"en": "Package", "ru": "Пакет"},
"scan_info_version": {"en": "Version", "ru": "Версия"}, "scan_info_version": {"en": "Version", "ru": "Версия"},
"scan_info_ecosystem": {"en": "Ecosystem", "ru": "Экосистема"}, "scan_info_ecosystem": {"en": "Ecosystem", "ru": "Экосистема"},
"scan_info_repository": {"en": "Repository", "ru": "Репозиторий"}, "scan_info_repository": {"en": "Repository", "ru": "Репозиторий"},
"scan_info_status": {"en": "Status", "ru": "Статус"}, "scan_info_status": {"en": "Status", "ru": "Статус"},
"scan_info_sha256": {"en": "SHA256", "ru": "SHA256"}, "scan_info_sha256": {"en": "SHA256", "ru": "SHA256"},
"scan_info_started": {"en": "Started", "ru": "Начат"}, "scan_info_started": {"en": "Started", "ru": "Запущено"},
"scan_info_finished": {"en": "Finished", "ru": "Завершён"}, "scan_info_finished": {"en": "Finished", "ru": "Завершено"},
"scan_info_initiated": {"en": "Initiated by", "ru": "Инициатор"}, "scan_info_initiated": {"en": "Initiated by", "ru": "Инициатор"},
"scan_info_source_ip": {"en": "Source IP", "ru": "IP-адрес"}, "scan_info_source_ip": {"en": "Source IP", "ru": "IP-адрес"},
"scan_info_error": {"en": "Error", "ru": "Ошибка"}, "scan_info_error": {"en": "Error", "ru": "Ошибка"},
"empty_no_scans": { "empty_no_scans": {
"en": "No scans yet — scans will appear here once packages are processed.", "en": "No scans yet — scans will appear here once packages are processed.",
"ru": "Сканов пока нет — появятся после обработки пакетов.", "ru": "Сканирования отсутствуют — появятся после обработки пакетов.",
}, },
"empty_no_packages": { "empty_no_packages": {
"en": "No packages yet — packages will appear here once scans are processed.", "en": "No packages yet — packages will appear here once scans are processed.",
"ru": "Пакетов пока нет — появятся после сканирования.", "ru": "Пакеты отсутствуют — появятся после сканирования.",
}, },
"empty_no_findings": { "empty_no_findings": {
"en": "No findings — package looks clean.", "en": "No findings — package looks clean.",
"ru": "Находок нет — пакет чистый.", "ru": "Находки отсутствуют — пакет не содержит угроз.",
}, },
"view_all_scans": {"en": "View all scans →", "ru": "Все сканы"}, "view_all_scans": {"en": "View all scans →", "ru": "Все сканирования"},
"refresh_hint": { "refresh_hint": {
"en": "Last refresh: {} (auto every 30s)", "en": "Last refresh: {} (auto every 30s)",
"ru": "Обновлено: {} (авто каждые 30с)", "ru": "Последнее обновление: {} (автоматически каждые 30 с)",
}, },
"llm_disabled": {"en": "LLM analysis is disabled", "ru": "LLM-анализ отключён"}, "llm_disabled": {"en": "LLM analysis is disabled", "ru": "LLM-анализ отключён"},
"llm_failed": {"en": "LLM analysis failed", "ru": "LLM-анализ не удался"}, "llm_failed": {"en": "LLM analysis failed", "ru": "Ошибка LLM-анализа"},
"llm_not_found": {"en": "Finding not found", "ru": "Находка не найдена"}, "llm_not_found": {"en": "Finding not found", "ru": "Находка не найдена"},
"llm_disclaimer": { "llm_disclaimer": {
"en": "⚠ AI-generated analysis — may contain inaccuracies. " "en": "⚠ AI-generated analysis — may contain inaccuracies. "
@@ -80,27 +83,27 @@ _STRINGS = {
"Всегда проверяйте находки перед принятием мер.", "Всегда проверяйте находки перед принятием мер.",
}, },
"breadcrumb_home": {"en": "Home", "ru": "Главная"}, "breadcrumb_home": {"en": "Home", "ru": "Главная"},
"breadcrumb_scans": {"en": "Scans", "ru": "Сканы"}, "breadcrumb_dashboard": {"en": "Dashboard", "ru": "Панель"},
"breadcrumb_scans": {"en": "Scans", "ru": "Сканирования"},
"breadcrumb_packages": {"en": "Packages", "ru": "Пакеты"}, "breadcrumb_packages": {"en": "Packages", "ru": "Пакеты"},
"breadcrumb_dashboard": {"en": "Dashboard", "ru": "Дашборд"},
"page_of": {"en": "of", "ru": "из"}, "page_of": {"en": "of", "ru": "из"},
"total_scans": {"en": "{} total scans", "ru": "{} всего сканов"}, "page_label": {"en": "Page", "ru": "Стр."},
"total_packages": {"en": "{} total packages", "ru": "{} всего пакетов"}, "total_scans": {"en": "{} total scans", "ru": "{} сканирований всего"},
"scan_detail_title": {"en": "Scan #{}", "ru": "Скан #{}"}, "total_packages": {"en": "{} total packages", "ru": "{} пакетов всего"},
"status_pending": {"en": "pending", "ru": "ожидает"}, "scan_detail_title": {"en": "Scan #{}", "ru": "Сканирование #{}"},
"status_scanning": {"en": "scanning", "ru": "сканируется"},
"status_completed": {"en": "completed", "ru": "завершён"},
"status_failed": {"en": "failed", "ru": "ошибка"},
} }
LANGUAGES = {"en": "English", "ru": "Русский"} LANGUAGES = {"en": "English", "ru": "Русский"}
DEFAULT_LANG = "en" DEFAULT_LANG = "en"
def t(key: str, lang: str = DEFAULT_LANG, **kwargs) -> str: def t(key: str, lang: str = DEFAULT_LANG, *args, **kwargs) -> str:
"""Look up a string in the given language, with optional formatting.""" """Look up a string in the given language, with optional formatting."""
entry = _STRINGS.get(key, {}) entry = _STRINGS.get(key, {})
text = entry.get(lang) or entry.get(DEFAULT_LANG, key) text = entry.get(lang) or entry.get(DEFAULT_LANG, key)
if kwargs: if args or kwargs:
return text.format(**kwargs) try:
return text.format(*args, **kwargs)
except (KeyError, IndexError):
return text
return text return text

View File

@@ -45,9 +45,9 @@ async def dashboard(request: Request, session: AsyncSession = Depends(get_sessio
@router.get("/dashboard/stats", response_class=HTMLResponse) @router.get("/dashboard/stats", response_class=HTMLResponse)
async def dashboard_stats_fragment(session: AsyncSession = Depends(get_session)): async def dashboard_stats_fragment(request: Request, session: AsyncSession = Depends(get_session)):
ctx = await get_dashboard_stats(session) ctx = await get_dashboard_stats(session)
return _render("dashboard_stats.html", **ctx) return _render("dashboard_stats.html", request=request, **ctx)
@router.get("/scans", response_class=HTMLResponse) @router.get("/scans", response_class=HTMLResponse)

View File

@@ -2,19 +2,19 @@
<thead> <thead>
<tr> <tr>
<th class="sortable {% if sort_by == 'name' %}active{% endif %}" hx-get="/packages?page=1&flagged={{ flagged_filter }}&search={{ search }}&sort_by=name&sort_dir={% if sort_by == 'name' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#packages-table-container" hx-swap="innerHTML"> <th class="sortable {% if sort_by == 'name' %}active{% endif %}" hx-get="/packages?page=1&flagged={{ flagged_filter }}&search={{ search }}&sort_by=name&sort_dir={% if sort_by == 'name' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#packages-table-container" hx-swap="innerHTML">
Name <span class="sort-icon">{% if sort_by == 'name' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span> {{ t('col_name', request.state.lang) }} <span class="sort-icon">{% if sort_by == 'name' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span>
</th> </th>
<th>Version</th> <th>{{ t('col_version', request.state.lang) }}</th>
<th>Ecosystem</th> <th>{{ t('col_ecosystem', request.state.lang) }}</th>
<th>Repo</th> <th>{{ t('col_repo', request.state.lang) }}</th>
<th class="sortable {% if sort_by == 'flagged' %}active{% endif %}" hx-get="/packages?page=1&flagged={{ flagged_filter }}&search={{ search }}&sort_by=flagged&sort_dir={% if sort_by == 'flagged' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#packages-table-container" hx-swap="innerHTML"> <th class="sortable {% if sort_by == 'flagged' %}active{% endif %}" hx-get="/packages?page=1&flagged={{ flagged_filter }}&search={{ search }}&sort_by=flagged&sort_dir={% if sort_by == 'flagged' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#packages-table-container" hx-swap="innerHTML">
Flagged <span class="sort-icon">{% if sort_by == 'flagged' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span> {{ t('col_flagged', request.state.lang) }} <span class="sort-icon">{% if sort_by == 'flagged' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span>
</th> </th>
<th class="sortable {% if sort_by == 'total_findings' %}active{% endif %}" hx-get="/packages?page=1&flagged={{ flagged_filter }}&search={{ search }}&sort_by=total_findings&sort_dir={% if sort_by == 'total_findings' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#packages-table-container" hx-swap="innerHTML"> <th class="sortable {% if sort_by == 'total_findings' %}active{% endif %}" hx-get="/packages?page=1&flagged={{ flagged_filter }}&search={{ search }}&sort_by=total_findings&sort_dir={% if sort_by == 'total_findings' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#packages-table-container" hx-swap="innerHTML">
Findings <span class="sort-icon">{% if sort_by == 'total_findings' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span> {{ t('col_findings', request.state.lang) }} <span class="sort-icon">{% if sort_by == 'total_findings' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span>
</th> </th>
<th class="sortable {% if sort_by == 'last_scanned_at' %}active{% endif %}" hx-get="/packages?page=1&flagged={{ flagged_filter }}&search={{ search }}&sort_by=last_scanned_at&sort_dir={% if sort_by == 'last_scanned_at' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#packages-table-container" hx-swap="innerHTML"> <th class="sortable {% if sort_by == 'last_scanned_at' %}active{% endif %}" hx-get="/packages?page=1&flagged={{ flagged_filter }}&search={{ search }}&sort_by=last_scanned_at&sort_dir={% if sort_by == 'last_scanned_at' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#packages-table-container" hx-swap="innerHTML">
Last Scan <span class="sort-icon">{% if sort_by == 'last_scanned_at' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span> {{ t('col_last_scan', request.state.lang) }} <span class="sort-icon">{% if sort_by == 'last_scanned_at' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span>
</th> </th>
</tr> </tr>
</thead> </thead>
@@ -32,7 +32,7 @@
{% endfor %} {% endfor %}
{% if not packages %} {% if not packages %}
<tr> <tr>
<td colspan="7" class="empty-state">No packages yet — packages will appear here once scans are processed.</td> <td colspan="7" class="empty-state">{{ t('empty_no_packages', request.state.lang) }}</td>
</tr> </tr>
{% endif %} {% endif %}
</tbody> </tbody>
@@ -42,10 +42,10 @@
{% if total_pages > 1 %} {% if total_pages > 1 %}
<nav> <nav>
<ul> <ul>
<li>{% if page > 1 %}<a href="?page={{ page - 1 }}&flagged={{ flagged_filter }}&search={{ search }}&sort_by={{ sort_by }}&sort_dir={{ sort_dir }}">Prev</a>{% else %}<span>Prev</span>{% endif %}</li> <li>{% if page > 1 %}<a href="?page={{ page - 1 }}&flagged={{ flagged_filter }}&search={{ search }}&sort_by={{ sort_by }}&sort_dir={{ sort_dir }}">{{ t('btn_prev', request.state.lang) }}</a>{% else %}<span>{{ t('btn_prev', request.state.lang) }}</span>{% endif %}</li>
<li><small>Page {{ page }} of {{ total_pages }}</small></li> <li><small>{{ t('page_label', request.state.lang) }} {{ page }} {{ t('page_of', request.state.lang) }} {{ total_pages }}</small></li>
<li>{% if page < total_pages %}<a href="?page={{ page + 1 }}&flagged={{ flagged_filter }}&search={{ search }}&sort_by={{ sort_by }}&sort_dir={{ sort_dir }}">Next</a>{% else %}<span>Next</span>{% endif %}</li> <li>{% if page < total_pages %}<a href="?page={{ page + 1 }}&flagged={{ flagged_filter }}&search={{ search }}&sort_by={{ sort_by }}&sort_dir={{ sort_dir }}">{{ t('btn_next', request.state.lang) }}</a>{% else %}<span>{{ t('btn_next', request.state.lang) }}</span>{% endif %}</li>
</ul> </ul>
</nav> </nav>
{% endif %} {% endif %}
<small style="opacity: 0.5;">{{ total }} total packages</small> <small style="opacity: 0.5;">{{ t('total_packages', request.state.lang, total) }}</small>

View File

@@ -2,21 +2,21 @@
<thead> <thead>
<tr> <tr>
<th class="sortable {% if sort_by == 'id' %}active{% endif %}" hx-get="/scans?page=1&flagged={{ flagged_filter }}&search={{ search }}&status={{ status_filter }}&sort_by=id&sort_dir={% if sort_by == 'id' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#scans-table-container" hx-swap="innerHTML"> <th class="sortable {% if sort_by == 'id' %}active{% endif %}" hx-get="/scans?page=1&flagged={{ flagged_filter }}&search={{ search }}&status={{ status_filter }}&sort_by=id&sort_dir={% if sort_by == 'id' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#scans-table-container" hx-swap="innerHTML">
ID <span class="sort-icon">{% if sort_by == 'id' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span> {{ t('col_id', request.state.lang) }} <span class="sort-icon">{% if sort_by == 'id' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span>
</th> </th>
<th class="sortable {% if sort_by == 'package_name' %}active{% endif %}" hx-get="/scans?page=1&flagged={{ flagged_filter }}&search={{ search }}&status={{ status_filter }}&sort_by=package_name&sort_dir={% if sort_by == 'package_name' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#scans-table-container" hx-swap="innerHTML"> <th class="sortable {% if sort_by == 'package_name' %}active{% endif %}" hx-get="/scans?page=1&flagged={{ flagged_filter }}&search={{ search }}&status={{ status_filter }}&sort_by=package_name&sort_dir={% if sort_by == 'package_name' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#scans-table-container" hx-swap="innerHTML">
Package <span class="sort-icon">{% if sort_by == 'package_name' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span> {{ t('col_package', request.state.lang) }} <span class="sort-icon">{% if sort_by == 'package_name' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span>
</th> </th>
<th>Version</th> <th>{{ t('col_version', request.state.lang) }}</th>
<th>Repo</th> <th>{{ t('col_repo', request.state.lang) }}</th>
<th class="sortable {% if sort_by == 'status' %}active{% endif %}" hx-get="/scans?page=1&flagged={{ flagged_filter }}&search={{ search }}&status={{ status_filter }}&sort_by=status&sort_dir={% if sort_by == 'status' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#scans-table-container" hx-swap="innerHTML"> <th class="sortable {% if sort_by == 'status' %}active{% endif %}" hx-get="/scans?page=1&flagged={{ flagged_filter }}&search={{ search }}&status={{ status_filter }}&sort_by=status&sort_dir={% if sort_by == 'status' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#scans-table-container" hx-swap="innerHTML">
Status <span class="sort-icon">{% if sort_by == 'status' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span> {{ t('col_status', request.state.lang) }} <span class="sort-icon">{% if sort_by == 'status' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span>
</th> </th>
<th class="sortable {% if sort_by == 'total_findings' %}active{% endif %}" hx-get="/scans?page=1&flagged={{ flagged_filter }}&search={{ search }}&status={{ status_filter }}&sort_by=total_findings&sort_dir={% if sort_by == 'total_findings' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#scans-table-container" hx-swap="innerHTML"> <th class="sortable {% if sort_by == 'total_findings' %}active{% endif %}" hx-get="/scans?page=1&flagged={{ flagged_filter }}&search={{ search }}&status={{ status_filter }}&sort_by=total_findings&sort_dir={% if sort_by == 'total_findings' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#scans-table-container" hx-swap="innerHTML">
Findings <span class="sort-icon">{% if sort_by == 'total_findings' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span> {{ t('col_findings', request.state.lang) }} <span class="sort-icon">{% if sort_by == 'total_findings' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span>
</th> </th>
<th class="sortable {% if sort_by == 'started_at' %}active{% endif %}" hx-get="/scans?page=1&flagged={{ flagged_filter }}&search={{ search }}&status={{ status_filter }}&sort_by=started_at&sort_dir={% if sort_by == 'started_at' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#scans-table-container" hx-swap="innerHTML"> <th class="sortable {% if sort_by == 'started_at' %}active{% endif %}" hx-get="/scans?page=1&flagged={{ flagged_filter }}&search={{ search }}&status={{ status_filter }}&sort_by=started_at&sort_dir={% if sort_by == 'started_at' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#scans-table-container" hx-swap="innerHTML">
Time <span class="sort-icon">{% if sort_by == 'started_at' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span> {{ t('col_time', request.state.lang) }} <span class="sort-icon">{% if sort_by == 'started_at' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span>
</th> </th>
</tr> </tr>
</thead> </thead>
@@ -36,7 +36,7 @@
{% endfor %} {% endfor %}
{% if not scans %} {% if not scans %}
<tr> <tr>
<td colspan="7" class="empty-state">No scans yet — scans will appear here once packages are processed.</td> <td colspan="7" class="empty-state">{{ t('empty_no_scans', request.state.lang) }}</td>
</tr> </tr>
{% endif %} {% endif %}
</tbody> </tbody>
@@ -46,10 +46,10 @@
{% if total_pages > 1 %} {% if total_pages > 1 %}
<nav> <nav>
<ul> <ul>
<li>{% if page > 1 %}<a href="?page={{ page - 1 }}&flagged={{ flagged_filter }}&search={{ search }}&status={{ status_filter }}&sort_by={{ sort_by }}&sort_dir={{ sort_dir }}">Prev</a>{% else %}<span>Prev</span>{% endif %}</li> <li>{% if page > 1 %}<a href="?page={{ page - 1 }}&flagged={{ flagged_filter }}&search={{ search }}&status={{ status_filter }}&sort_by={{ sort_by }}&sort_dir={{ sort_dir }}">{{ t('btn_prev', request.state.lang) }}</a>{% else %}<span>{{ t('btn_prev', request.state.lang) }}</span>{% endif %}</li>
<li><small>Page {{ page }} of {{ total_pages }}</small></li> <li><small>{{ t('page_label', request.state.lang) }} {{ page }} {{ t('page_of', request.state.lang) }} {{ total_pages }}</small></li>
<li>{% if page < total_pages %}<a href="?page={{ page + 1 }}&flagged={{ flagged_filter }}&search={{ search }}&status={{ status_filter }}&sort_by={{ sort_by }}&sort_dir={{ sort_dir }}">Next</a>{% else %}<span>Next</span>{% endif %}</li> <li>{% if page < total_pages %}<a href="?page={{ page + 1 }}&flagged={{ flagged_filter }}&search={{ search }}&status={{ status_filter }}&sort_by={{ sort_by }}&sort_dir={{ sort_dir }}">{{ t('btn_next', request.state.lang) }}</a>{% else %}<span>{{ t('btn_next', request.state.lang) }}</span>{% endif %}</li>
</ul> </ul>
</nav> </nav>
{% endif %} {% endif %}
<small style="opacity: 0.5;">{{ total }} total scans</small> <small style="opacity: 0.5;">{{ t('total_scans', request.state.lang, total) }}</small>

View File

@@ -1,14 +1,14 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Dashboard — GuardDog Nexus{% endblock %} {% block title %}{{ t('title_dashboard', request.state.lang) }}{% endblock %}
{% block breadcrumbs %} {% block breadcrumbs %}
<div class="breadcrumbs"> <div class="breadcrumbs">
<a href="/">Home</a> <a href="/">{{ t('breadcrumb_home', request.state.lang) }}</a>
<span class="separator">/</span> <span class="separator">/</span>
<span>Dashboard</span> <span>{{ t('breadcrumb_dashboard', request.state.lang) }}</span>
</div> </div>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h1>Dashboard</h1> <h1>{{ t('heading_dashboard', request.state.lang) }}</h1>
<div hx-get="/dashboard/stats" hx-trigger="every 30s" hx-swap="innerHTML"> <div hx-get="/dashboard/stats" hx-trigger="every 30s" hx-swap="innerHTML">
{% include "dashboard_stats.html" %} {% include "dashboard_stats.html" %}

View File

@@ -1,8 +1,8 @@
{% if latest_flagged %} {% if latest_flagged %}
<article class="dash-block dash-block-warn"> <article class="dash-block dash-block-warn">
<h3>Latest Flagged</h3> <h3>{{ t('heading_latest_flagged', request.state.lang) }}</h3>
<table class="compact"> <table class="compact">
<thead><tr><th>Package</th><th>Version</th><th>Findings</th><th>Time</th></tr></thead> <thead><tr><th>{{ t('col_package', request.state.lang) }}</th><th>{{ t('col_version', request.state.lang) }}</th><th>{{ t('col_findings', request.state.lang) }}</th><th>{{ t('col_time', request.state.lang) }}</th></tr></thead>
<tbody> <tbody>
{% for s in latest_flagged %} {% for s in latest_flagged %}
<tr> <tr>
@@ -18,10 +18,10 @@
{% endif %} {% endif %}
<article class="dash-block" style="margin-top: 0;"> <article class="dash-block" style="margin-top: 0;">
<h3>Latest Scans</h3> <h3>{{ t('heading_latest_scans', request.state.lang) }}</h3>
<table class="compact"> <table class="compact">
<thead> <thead>
<tr><th>Package</th><th>Version</th><th>Repo</th><th>Status</th><th></th><th>Time</th></tr> <tr><th>{{ t('col_package', request.state.lang) }}</th><th>{{ t('col_version', request.state.lang) }}</th><th>{{ t('col_repo', request.state.lang) }}</th><th>{{ t('col_status', request.state.lang) }}</th><th></th><th>{{ t('col_time', request.state.lang) }}</th></tr>
</thead> </thead>
<tbody> <tbody>
{% for s in latest_scans %} {% for s in latest_scans %}
@@ -38,7 +38,7 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<small><a href="/scans">View all scans</a></small> <small><a href="/scans">{{ t('view_all_scans', request.state.lang) }}</a></small>
</article> </article>
<small style="opacity: 0.4;">Last refresh: {{ now.strftime('%H:%M:%S') }} (auto every 30s)</small> <small style="opacity: 0.4;">{{ t('refresh_hint', request.state.lang, now.strftime('%H:%M:%S')) }}</small>

View File

@@ -1,10 +1,10 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ pkg_name }} v{{ pkg_version }} — GuardDog Nexus{% endblock %} {% block title %}{{ t('title_package_detail', request.state.lang, pkg_name, pkg_version) }}{% endblock %}
{% block breadcrumbs %} {% block breadcrumbs %}
<div class="breadcrumbs"> <div class="breadcrumbs">
<a href="/">Home</a> <a href="/">{{ t('breadcrumb_home', request.state.lang) }}</a>
<span class="separator">/</span> <span class="separator">/</span>
<a href="/packages">Packages</a> <a href="/packages">{{ t('breadcrumb_packages', request.state.lang) }}</a>
<span class="separator">/</span> <span class="separator">/</span>
<span>{{ pkg_name }} v{{ pkg_version }}</span> <span>{{ pkg_name }} v{{ pkg_version }}</span>
</div> </div>
@@ -13,10 +13,10 @@
<h1>{{ pkg_name }} <small>v{{ pkg_version }}</small></h1> <h1>{{ pkg_name }} <small>v{{ pkg_version }}</small></h1>
<article class="scan-info-block"> <article class="scan-info-block">
<h2>Scans ({{ scans|length }})</h2> <h2>{{ t('heading_scans_count', request.state.lang, scans|length) }}</h2>
<table class="compact"> <table class="compact">
<thead> <thead>
<tr><th>ID</th><th>Repo</th><th>Status</th><th>Findings</th><th>Time</th></tr> <tr><th>{{ t('col_id', request.state.lang) }}</th><th>{{ t('col_repo', request.state.lang) }}</th><th>{{ t('col_status', request.state.lang) }}</th><th>{{ t('col_findings', request.state.lang) }}</th><th>{{ t('col_time', request.state.lang) }}</th></tr>
</thead> </thead>
<tbody> <tbody>
{% for s in scans %} {% for s in scans %}
@@ -34,7 +34,7 @@
</table> </table>
</article> </article>
<h2 style="margin-bottom: 0.75rem;">Findings ({{ findings|length }})</h2> <h2 style="margin-bottom: 0.75rem;">{{ t('heading_findings_count', request.state.lang, findings|length) }}</h2>
{% if findings %} {% if findings %}
<div id="findings-container"> <div id="findings-container">
@@ -49,7 +49,7 @@
<p>{{ f.data.message }}</p> <p>{{ f.data.message }}</p>
{% if f.data.code %} {% if f.data.code %}
<div class="code-toolbar"> <div class="code-toolbar">
<button class="copy-btn" onclick="copyCode(this, 'code-{{ f.id }}')">Copy</button> <button class="copy-btn" onclick="copyCode(this, 'code-{{ f.id }}')">{{ t('btn_copy', request.state.lang) }}</button>
</div> </div>
<pre><code id="code-{{ f.id }}">{{ f.data.code }}</code></pre> <pre><code id="code-{{ f.id }}">{{ f.data.code }}</code></pre>
{% endif %} {% endif %}
@@ -64,7 +64,7 @@
</div> </div>
<p class="llm-summary">{{ f.report.summary }}</p> <p class="llm-summary">{{ f.report.summary }}</p>
<p class="llm-analysis">{{ f.report.analysis }}</p> <p class="llm-analysis">{{ f.report.analysis }}</p>
<p class="llm-disclaimer">⚠ AI-generated analysis — may contain inaccuracies.</p> <p class="llm-disclaimer">{{ t('llm_disclaimer', request.state.lang) }}</p>
</div> </div>
{% else %} {% else %}
<div class="llm-actions" id="llm-{{ f.id }}"> <div class="llm-actions" id="llm-{{ f.id }}">
@@ -76,7 +76,7 @@
<span id="llm-spinner-{{ f.id }}" class="htmx-indicator" style="display:none;"> <span id="llm-spinner-{{ f.id }}" class="htmx-indicator" style="display:none;">
<span class="spinner"></span> <span class="spinner"></span>
</span> </span>
Analyze with LLM {{ t('btn_analyze_llm', request.state.lang) }}
</button> </button>
</div> </div>
{% endif %} {% endif %}
@@ -85,6 +85,6 @@
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}
<p class="empty-state">No findings — package looks clean.</p> <p class="empty-state">{{ t('empty_no_findings', request.state.lang) }}</p>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@@ -1,21 +1,21 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Packages — GuardDog Nexus{% endblock %} {% block title %}{{ t('title_packages', request.state.lang) }}{% endblock %}
{% block breadcrumbs %} {% block breadcrumbs %}
<div class="breadcrumbs"> <div class="breadcrumbs">
<a href="/">Home</a> <a href="/">{{ t('breadcrumb_home', request.state.lang) }}</a>
<span class="separator">/</span> <span class="separator">/</span>
<span>Packages</span> <span>{{ t('breadcrumb_packages', request.state.lang) }}</span>
</div> </div>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h1>Packages</h1> <h1>{{ t('heading_packages', request.state.lang) }}</h1>
<div class="filter-bar"> <div class="filter-bar">
<input type="text" name="search" placeholder="Search packages..." value="{{ search }}" hx-get="/packages" hx-trigger="input changed, keyup[entered] delay:300ms" hx-target="#packages-table-container" hx-swap="innerHTML"> <input type="text" name="search" placeholder="{{ t('filter_search', request.state.lang) }}" value="{{ search }}" hx-get="/packages" hx-trigger="input changed, keyup[entered] delay:300ms" hx-target="#packages-table-container" hx-swap="innerHTML">
<a href="?flagged={% if flagged_filter == '1' %}0{% else %}1{% endif %}" role="button" class="outline"> <a href="?flagged={% if flagged_filter == '1' %}0{% else %}1{% endif %}" role="button" class="outline">
{% if flagged_filter == '1' %}Show all{% else %}Flagged only{% endif %} {% if flagged_filter == '1' %}{{ t('btn_show_all', request.state.lang) }}{% else %}{{ t('btn_flagged_only', request.state.lang) }}{% endif %}
</a> </a>
<a href="/api/v1/packages/export?flagged={{ flagged_filter }}&search={{ search }}" role="button" class="outline">Export CSV</a> <a href="/api/v1/packages/export?flagged={{ flagged_filter }}&search={{ search }}" role="button" class="outline">{{ t('btn_export_csv', request.state.lang) }}</a>
</div> </div>
<div id="packages-table-container"> <div id="packages-table-container">

View File

@@ -1,36 +1,36 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Scan #{{ scan.id }} — GuardDog Nexus{% endblock %} {% block title %}{{ t('title_scan_detail', request.state.lang, scan.id) }}{% endblock %}
{% block breadcrumbs %} {% block breadcrumbs %}
<div class="breadcrumbs"> <div class="breadcrumbs">
<a href="/">Home</a> <a href="/">{{ t('breadcrumb_home', request.state.lang) }}</a>
<span class="separator">/</span> <span class="separator">/</span>
<a href="/scans">Scans</a> <a href="/scans">{{ t('breadcrumb_scans', request.state.lang) }}</a>
<span class="separator">/</span> <span class="separator">/</span>
<span>Scan #{{ scan.id }}</span> <span>{{ t('scan_detail_title', request.state.lang, scan.id) }}</span>
</div> </div>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h1>Scan #{{ scan.id }}</h1> <h1>{{ t('scan_detail_title', request.state.lang, scan.id) }}</h1>
<article class="scan-info-block"> <article class="scan-info-block">
<div class="scan-info-grid"> <div class="scan-info-grid">
<div><strong>Package</strong><br><a href="/packages/{{ scan.package_name | urlencode }}/{{ scan.package_version | urlencode }}">{{ scan.package_name }}</a></div> <div><strong>{{ t('scan_info_package', request.state.lang) }}</strong><br><a href="/packages/{{ scan.package_name | urlencode }}/{{ scan.package_version | urlencode }}">{{ scan.package_name }}</a></div>
<div><strong>Version</strong><br>{{ scan.package_version }}</div> <div><strong>{{ t('scan_info_version', request.state.lang) }}</strong><br>{{ scan.package_version }}</div>
<div><strong>Ecosystem</strong><br>{{ scan.ecosystem }}</div> <div><strong>{{ t('scan_info_ecosystem', request.state.lang) }}</strong><br>{{ scan.ecosystem }}</div>
<div><strong>Repository</strong><br>{{ scan.repository }}</div> <div><strong>{{ t('scan_info_repository', request.state.lang) }}</strong><br>{{ scan.repository }}</div>
<div><strong>Status</strong><br> <div><strong>{{ t('scan_info_status', request.state.lang) }}</strong><br>
{% if scan.status == 'scanning' %}<span class="status-scanning"><span class="spinner"></span>scanning</span>{% else %}<span class="status-{{ scan.status }}">{{ scan.status }}</span>{% endif %} {% if scan.status == 'scanning' %}<span class="status-scanning"><span class="spinner"></span>scanning</span>{% else %}<span class="status-{{ scan.status }}">{{ scan.status }}</span>{% endif %}
</div> </div>
<div><strong>SHA256</strong><br><code class="sha256">{{ scan.sha256 or '-' }}</code></div> <div><strong>{{ t('scan_info_sha256', request.state.lang) }}</strong><br><code class="sha256">{{ scan.sha256 or '-' }}</code></div>
<div><strong>Started</strong><br>{{ scan.started_at.strftime('%Y-%m-%d %H:%M') if scan.started_at }}</div> <div><strong>{{ t('scan_info_started', request.state.lang) }}</strong><br>{{ scan.started_at.strftime('%Y-%m-%d %H:%M') if scan.started_at }}</div>
<div><strong>Finished</strong><br>{{ scan.finished_at.strftime('%Y-%m-%d %H:%M') if scan.finished_at }}</div> <div><strong>{{ t('scan_info_finished', request.state.lang) }}</strong><br>{{ scan.finished_at.strftime('%Y-%m-%d %H:%M') if scan.finished_at }}</div>
{% if scan.initiator %}<div><strong>Initiated by</strong><br>{{ scan.initiator }}</div>{% endif %} {% if scan.initiator %}<div><strong>{{ t('scan_info_initiated', request.state.lang) }}</strong><br>{{ scan.initiator }}</div>{% endif %}
{% if scan.source_ip %}<div><strong>Source IP</strong><br>{{ scan.source_ip }}</div>{% endif %} {% if scan.source_ip %}<div><strong>{{ t('scan_info_source_ip', request.state.lang) }}</strong><br>{{ scan.source_ip }}</div>{% endif %}
</div> </div>
{% if scan.error_message %}<div class="scan-error"><strong>Error:</strong> {{ scan.error_message }}</div>{% endif %} {% if scan.error_message %}<div class="scan-error"><strong>{{ t('scan_info_error', request.state.lang) }}:</strong> {{ scan.error_message }}</div>{% endif %}
</article> </article>
<h2 style="margin-bottom: 0.75rem;">Findings ({{ scan.findings|length }})</h2> <h2 style="margin-bottom: 0.75rem;">{{ t('heading_findings_count', request.state.lang, scan.findings|length) }}</h2>
{% if scan.findings %} {% if scan.findings %}
<div id="findings-container"> <div id="findings-container">
@@ -45,7 +45,7 @@
<p>{{ f.data.message }}</p> <p>{{ f.data.message }}</p>
{% if f.data.code %} {% if f.data.code %}
<div class="code-toolbar"> <div class="code-toolbar">
<button class="copy-btn" onclick="copyCode(this, 'code-{{ f.id }}')">Copy</button> <button class="copy-btn" onclick="copyCode(this, 'code-{{ f.id }}')">{{ t('btn_copy', request.state.lang) }}</button>
</div> </div>
<pre><code id="code-{{ f.id }}">{{ f.data.code }}</code></pre> <pre><code id="code-{{ f.id }}">{{ f.data.code }}</code></pre>
{% endif %} {% endif %}
@@ -60,7 +60,7 @@
</div> </div>
<p class="llm-summary">{{ f.report.summary }}</p> <p class="llm-summary">{{ f.report.summary }}</p>
<p class="llm-analysis">{{ f.report.analysis }}</p> <p class="llm-analysis">{{ f.report.analysis }}</p>
<p class="llm-disclaimer">⚠ AI-generated analysis — may contain inaccuracies.</p> <p class="llm-disclaimer">{{ t('llm_disclaimer', request.state.lang) }}</p>
</div> </div>
{% else %} {% else %}
<div class="llm-actions" id="llm-{{ f.id }}"> <div class="llm-actions" id="llm-{{ f.id }}">
@@ -72,7 +72,7 @@
<span id="llm-spinner-{{ f.id }}" class="htmx-indicator" style="display:none;"> <span id="llm-spinner-{{ f.id }}" class="htmx-indicator" style="display:none;">
<span class="spinner"></span> <span class="spinner"></span>
</span> </span>
Analyze with LLM {{ t('btn_analyze_llm', request.state.lang) }}
</button> </button>
</div> </div>
{% endif %} {% endif %}
@@ -81,6 +81,6 @@
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}
<p class="empty-state">No findings — package looks clean.</p> <p class="empty-state">{{ t('empty_no_findings', request.state.lang) }}</p>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@@ -1,28 +1,28 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Scans — GuardDog Nexus{% endblock %} {% block title %}{{ t('title_scans', request.state.lang) }}{% endblock %}
{% block breadcrumbs %} {% block breadcrumbs %}
<div class="breadcrumbs"> <div class="breadcrumbs">
<a href="/">Home</a> <a href="/">{{ t('breadcrumb_home', request.state.lang) }}</a>
<span class="separator">/</span> <span class="separator">/</span>
<span>Scans</span> <span>{{ t('breadcrumb_scans', request.state.lang) }}</span>
</div> </div>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h1>Scans</h1> <h1>{{ t('heading_scans', request.state.lang) }}</h1>
<div class="filter-bar"> <div class="filter-bar">
<input type="text" name="search" placeholder="Search packages..." value="{{ search }}" hx-get="/scans" hx-trigger="input changed, keyup[entered] delay:300ms" hx-target="#scans-table-container" hx-swap="innerHTML" hx-include="#status-filter"> <input type="text" name="search" placeholder="{{ t('filter_search', request.state.lang) }}" value="{{ search }}" hx-get="/scans" hx-trigger="input changed, keyup[entered] delay:300ms" hx-target="#scans-table-container" hx-swap="innerHTML" hx-include="#status-filter">
<select name="status" id="status-filter" hx-get="/scans" hx-trigger="change" hx-target="#scans-table-container" hx-swap="innerHTML" hx-include="[name=search]"> <select name="status" id="status-filter" hx-get="/scans" hx-trigger="change" hx-target="#scans-table-container" hx-swap="innerHTML" hx-include="[name=search]">
<option value="">All Statuses</option> <option value="">{{ t('filter_all_statuses', request.state.lang) }}</option>
<option value="pending" {% if status_filter == 'pending' %}selected{% endif %}>Pending</option> <option value="pending" {% if status_filter == 'pending' %}selected{% endif %}>{{ t('filter_pending', request.state.lang) }}</option>
<option value="scanning" {% if status_filter == 'scanning' %}selected{% endif %}>Scanning</option> <option value="scanning" {% if status_filter == 'scanning' %}selected{% endif %}>{{ t('filter_scanning', request.state.lang) }}</option>
<option value="completed" {% if status_filter == 'completed' %}selected{% endif %}>Completed</option> <option value="completed" {% if status_filter == 'completed' %}selected{% endif %}>{{ t('filter_completed', request.state.lang) }}</option>
<option value="failed" {% if status_filter == 'failed' %}selected{% endif %}>Failed</option> <option value="failed" {% if status_filter == 'failed' %}selected{% endif %}>{{ t('filter_failed', request.state.lang) }}</option>
</select> </select>
<a href="?flagged={% if flagged_filter == '1' %}0{% else %}1{% endif %}" role="button" class="outline"> <a href="?flagged={% if flagged_filter == '1' %}0{% else %}1{% endif %}" role="button" class="outline">
{% if flagged_filter == '1' %}Show all{% else %}Flagged only{% endif %} {% if flagged_filter == '1' %}{{ t('btn_show_all', request.state.lang) }}{% else %}{{ t('btn_flagged_only', request.state.lang) }}{% endif %}
</a> </a>
<a href="/api/v1/scans/export?flagged={{ flagged_filter }}&search={{ search }}&status={{ status_filter }}" role="button" class="outline">Export CSV</a> <a href="/api/v1/scans/export?flagged={{ flagged_filter }}&search={{ search }}&status={{ status_filter }}" role="button" class="outline">{{ t('btn_export_csv', request.state.lang) }}</a>
</div> </div>
<div id="scans-table-container"> <div id="scans-table-container">