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:
@@ -76,8 +76,10 @@ async def scans_list(
|
|||||||
scans = (await session.execute(q)).scalars().all()
|
scans = (await session.execute(q)).scalars().all()
|
||||||
total = await session.scalar(count_q)
|
total = await session.scalar(count_q)
|
||||||
|
|
||||||
|
template = "_scans_table.html" if request.headers.get("HX-Request") else "scans_list.html"
|
||||||
|
|
||||||
return _render(
|
return _render(
|
||||||
"scans_list.html",
|
template,
|
||||||
scans=scans,
|
scans=scans,
|
||||||
page=page,
|
page=page,
|
||||||
per_page=per_page,
|
per_page=per_page,
|
||||||
@@ -136,8 +138,10 @@ async def packages_list(
|
|||||||
total = await session.scalar(total_q)
|
total = await session.scalar(total_q)
|
||||||
rows = (await session.execute(rows_q)).all()
|
rows = (await session.execute(rows_q)).all()
|
||||||
|
|
||||||
|
template = "_packages_table.html" if request.headers.get("HX-Request") else "packages_list.html"
|
||||||
|
|
||||||
return _render(
|
return _render(
|
||||||
"packages_list.html",
|
template,
|
||||||
packages=rows,
|
packages=rows,
|
||||||
page=page,
|
page=page,
|
||||||
per_page=per_page,
|
per_page=per_page,
|
||||||
|
|||||||
61
guarddog_nexus/web/templates/_packages_table.html
Normal file
61
guarddog_nexus/web/templates/_packages_table.html
Normal 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>
|
||||||
72
guarddog_nexus/web/templates/_scans_table.html
Normal file
72
guarddog_nexus/web/templates/_scans_table.html
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<div class="filter-bar">
|
||||||
|
<input type="text" id="search-input" placeholder="Search packages..." value="{{ search }}" hx-get="/scans" hx-trigger="input changed, keyup[entered] delay:300ms" hx-target="#scans-table-container" hx-swap="innerHTML">
|
||||||
|
<select id="status-filter" hx-get="/scans" hx-trigger="change" hx-target="#scans-table-container" hx-swap="innerHTML">
|
||||||
|
<option value="">All Statuses</option>
|
||||||
|
<option value="pending" {% if status_filter == 'pending' %}selected{% endif %}>Pending</option>
|
||||||
|
<option value="scanning" {% if status_filter == 'scanning' %}selected{% endif %}>Scanning</option>
|
||||||
|
<option value="completed" {% if status_filter == 'completed' %}selected{% endif %}>Completed</option>
|
||||||
|
<option value="failed" {% if status_filter == 'failed' %}selected{% endif %}>Failed</option>
|
||||||
|
</select>
|
||||||
|
<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/scans/export?flagged={{ flagged_filter }}&search={{ search }}&status={{ status_filter }}" role="button" class="outline">Export CSV</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="scans-table-container">
|
||||||
|
<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">
|
||||||
|
ID <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">
|
||||||
|
Package <span class="sort-icon">{% if sort_by == 'package_name' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span>
|
||||||
|
</th>
|
||||||
|
<th>Version</th>
|
||||||
|
<th>Repo</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">
|
||||||
|
Status <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">
|
||||||
|
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 == '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>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for s in scans %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="/scans/{{ s.id }}">#{{ s.id }}</a></td>
|
||||||
|
<td>{{ s.package_name }}</td>
|
||||||
|
<td>{{ s.package_version }}</td>
|
||||||
|
<td>{{ s.repository }}</td>
|
||||||
|
<td>
|
||||||
|
{% if s.status == 'scanning' %}<span class="status-scanning"><span class="spinner"></span>scanning</span>{% else %}<span class="status-{{ s.status }}">{{ s.status }}</span>{% endif %}
|
||||||
|
</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">No scans yet — scans will appear here once packages 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 }}&status={{ status_filter }}&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 }}&status={{ status_filter }}&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 scans</small>
|
||||||
@@ -9,66 +9,5 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Packages</h1>
|
<h1>Packages</h1>
|
||||||
|
{% include "_packages_table.html" %}
|
||||||
<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>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -9,77 +9,5 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Scans</h1>
|
<h1>Scans</h1>
|
||||||
|
{% include "_scans_table.html" %}
|
||||||
<div class="filter-bar">
|
|
||||||
<input type="text" id="search-input" placeholder="Search packages..." value="{{ search }}" hx-get="/scans" hx-trigger="input changed, keyup[entered] delay:300ms" hx-target="#scans-table-container" hx-swap="innerHTML">
|
|
||||||
<select id="status-filter" hx-get="/scans" hx-trigger="change" hx-target="#scans-table-container" hx-swap="innerHTML">
|
|
||||||
<option value="">All Statuses</option>
|
|
||||||
<option value="pending" {% if status_filter == 'pending' %}selected{% endif %}>Pending</option>
|
|
||||||
<option value="scanning" {% if status_filter == 'scanning' %}selected{% endif %}>Scanning</option>
|
|
||||||
<option value="completed" {% if status_filter == 'completed' %}selected{% endif %}>Completed</option>
|
|
||||||
<option value="failed" {% if status_filter == 'failed' %}selected{% endif %}>Failed</option>
|
|
||||||
</select>
|
|
||||||
<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/scans/export?flagged={{ flagged_filter }}&search={{ search }}&status={{ status_filter }}" role="button" class="outline">Export CSV</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="scans-table-container">
|
|
||||||
<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">
|
|
||||||
ID <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">
|
|
||||||
Package <span class="sort-icon">{% if sort_by == 'package_name' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span>
|
|
||||||
</th>
|
|
||||||
<th>Version</th>
|
|
||||||
<th>Repo</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">
|
|
||||||
Status <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">
|
|
||||||
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 == '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>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for s in scans %}
|
|
||||||
<tr>
|
|
||||||
<td><a href="/scans/{{ s.id }}">#{{ s.id }}</a></td>
|
|
||||||
<td>{{ s.package_name }}</td>
|
|
||||||
<td>{{ s.package_version }}</td>
|
|
||||||
<td>{{ s.repository }}</td>
|
|
||||||
<td>
|
|
||||||
{% if s.status == 'scanning' %}<span class="status-scanning"><span class="spinner"></span>scanning</span>{% else %}<span class="status-{{ s.status }}">{{ s.status }}</span>{% endif %}
|
|
||||||
</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">No scans yet — scans will appear here once packages 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 }}&status={{ status_filter }}&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 }}&status={{ status_filter }}&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 scans</small>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user