- S4: bump jinja2>=3.1.4, python-multipart>=0.0.18, httpx>=0.28.0
- S5: _detect_ecosystem — DEFAULT_ECOSYSTEM для неизвестных форматов
- S6: harvester — log.exception() вместо log.error()
- S8: _scan_component — urlencode параметров
- P1: scanner — proc.kill() при таймауте
- P3: api_packages — selectinload(Scan.findings), убран N+1
- P4+P5: утечка _url_locks и _llm_locks при early return
- P6: DB reaper — сброс {'status':'analyzing'} при старте
- UI: htmx-пагинация, фильтры не теряют flagged, 404 с layout
- UI: мобильные таблицы overflow-x, полная стата на дашборде
- UI: i18n статусов в _status_badge, urlencode package_name
- 16 новых тестов: analyze endpoint (6), scanner errors (4),
webhook signature (2), llm client (4)
47 lines
3.9 KiB
HTML
47 lines
3.9 KiB
HTML
<table>
|
|
<thead>
|
|
<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">
|
|
{{ t('col_id', request.state.lang) }} <span class="sort-icon">{% if sort_by == 'id' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span>
|
|
</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">
|
|
{{ 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>{{ t('col_version', request.state.lang) }}</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">
|
|
{{ t('col_status', request.state.lang) }} <span class="sort-icon">{% if sort_by == 'status' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span>
|
|
</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">
|
|
{{ 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 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">
|
|
{{ t('col_time', request.state.lang) }} <span class="sort-icon">{% if sort_by == 'started_at' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span>
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for s in scans %}
|
|
<tr>
|
|
<td><a href="/scans/{{ s.id }}">#{{ s.id }}</a></td>
|
|
<td><a href="/packages/{{ s.package_name | urlencode }}/{{ s.package_version | urlencode }}">{{ s.package_name }}</a></td>
|
|
<td>{{ s.package_version }}</td>
|
|
<td>{{ s.repository }}</td>
|
|
<td>
|
|
{% with status=s.status %}{% include "_status_badge.html" %}{% endwith %}
|
|
</td>
|
|
<td>{% if s.flagged %}<span class="flagged">{{ s.total_findings }}</span>{% else %}<span class="clean">0</span>{% endif %}</td>
|
|
<td>{{ s.started_at.strftime('%Y-%m-%d %H:%M') if s.started_at }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
{% if not scans %}
|
|
<tr>
|
|
<td colspan="7" class="empty-state">{{ t('empty_no_scans', request.state.lang) }}</td>
|
|
</tr>
|
|
{% endif %}
|
|
</tbody>
|
|
</table>
|
|
|
|
{% include "_pagination.html" %}
|
|
<small style="opacity: 0.5;">{{ t('total_scans', request.state.lang, total) }}</small>
|