fix(ui): исправить раздвоение интерфейса при htmx-фильтрации

Проблема: htmx через hx-target="#scans-table-container" получал
полную HTML-страницу (с <html>, <nav>, <head>) и вставлял её внутрь
существующей страницы → дублировался header.

Решение: шаблоны разделены на полные + фрагменты:
  - _scans_table.html — только filter-bar + таблица + пагинация
  - _packages_table.html — аналогично
  - web/routes.py: проверка HX-Request хедера → отдаём фрагмент
This commit is contained in:
Marker689
2026-05-10 06:08:24 +03:00
parent d23abe8b4b
commit c4c27deb79
5 changed files with 141 additions and 137 deletions

View File

@@ -0,0 +1,61 @@
<div class="filter-bar">
<input type="text" id="search-input" placeholder="Search packages..." 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">
{% if flagged_filter == '1' %}Show all{% else %}Flagged only{% endif %}
</a>
<a href="/api/v1/packages/export?flagged={{ flagged_filter }}&search={{ search }}" role="button" class="outline">Export CSV</a>
</div>
<div id="packages-table-container">
<table>
<thead>
<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">
Name <span class="sort-icon">{% if sort_by == 'name' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span>
</th>
<th>Version</th>
<th>Ecosystem</th>
<th>Repo</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">
Flagged <span class="sort-icon">{% if sort_by == 'flagged' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span>
</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">
Findings <span class="sort-icon">{% if sort_by == 'total_findings' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span>
</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">
Last Scan <span class="sort-icon">{% if sort_by == 'last_scanned_at' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span>
</th>
</tr>
</thead>
<tbody>
{% for p in packages %}
<tr>
<td><a href="/packages/{{ p.pkg_name }}/{{ p.pkg_ver }}">{{ p.pkg_name }}</a></td>
<td>{{ p.pkg_ver }}</td>
<td>{{ p.ecosystem }}</td>
<td>{{ p.repository }}</td>
<td>{% if p.is_flagged %}<span class="flagged">YES</span>{% else %}<span class="clean">No</span>{% endif %}</td>
<td>{{ p.findings_sum }}</td>
<td>{{ p.last_scan.strftime('%Y-%m-%d %H:%M') if p.last_scan }}</td>
</tr>
{% endfor %}
{% if not packages %}
<tr>
<td colspan="7" class="empty-state">No packages yet — packages will appear here once scans are processed.</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
{% set total_pages = (total // per_page) + (1 if total % per_page else 0) %}
{% if total_pages > 1 %}
<nav>
<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><small>Page {{ page }} of {{ 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>
</ul>
</nav>
{% endif %}
<small style="opacity: 0.5;">{{ total }} total packages</small>